C#基礎之類型和成員基礎以及常量、字段、屬性


首先吐糟一下今天杭州的天氣,真是太熱了!雖然沒有妹子跟我約會,但宅在方寸大的窩里,也是煩躁不已!

接上一篇《C#基礎之基本類型》

類型和成員基礎

在C#中,一個類型內部可以定義多種成員:常量、字段、實例構造器、類型構造器(靜態構造器)、方法、操作符重載、轉換操作符、屬性、事件、類型。

類型的可見性publicinternal(默認)兩種,前者定義的類型對所有程序集中的所有類型都可見,后者定義的類型只對同一程序集內部的所有類型可見:

 public class PublicClass { }                //所有處可見
 internal class ExplicitlyInternalClass { }  //程序集內可見
 class ImplicitlyInternalClass { }           //程序集內可見(C#編譯器默認設置為internal)

成員的可訪問性(按限制從大到小排列):

  • Private只能由定義成員的類型或嵌套類型中方法訪問
  • Protected只能由定義成員的類型或嵌套類型或派生類型中方法訪問
  • Internal 只能由同程序集類型中方法訪問
  • Protected Internal 只能由定義成員的類型或嵌套類型或派生類型或同程序集類型中方法訪問(注意這里是或的關系)
  • Public 可由任何程序集中任何類型中方法訪問

在C#中,如果沒有顯式聲明成員的可訪問性,編譯器通常默認選擇Private(限制最大的那個),CLR要求接口類型的所有成員都是Public訪問性,C#編譯器知道這一點,因此禁止顯式指定接口成員的可訪問性。同時C#還要求在繼承過程中派生類重寫成員時,不能更改成員的可訪問性(CLR並沒有作這個要求,CLR允許重寫成員時放寬限制)。

靜態類

永遠不需要實例化的類,靜態類中只能有靜態成員。在C#中用static這個關鍵詞定義一個靜態類,但只能應用於class,不能應用於struct,因為CLR總是允許值類型實例化。

C#編譯器對靜態類作了如下限制:

  • 靜態類必須直接從System.Object派生
  • 靜態類不能實現任何接口(因為只有使用類的一個實例才能調用類的接口方法)
  • 靜態類只能定義靜態成員(字段、方法、屬性、事件)
  • 靜態類不能作為字段、方法參數或局部變量使用
  • 靜態類在編譯后,會生成一個被標記為abstract和sealed的類,同時編譯器不會生成實例構造器(.ctor方法)

分部類、結構和接口

C#編譯器提供一個partial關鍵字,以允許將一個類、結構或接口定義在多個文件里。

在編譯時,編譯器自動將類、結構或接口的各部分合並起來。這僅是C#編譯器提供的一個功能,CLR對此一無所知。

常量

常量就是代表一恆定數據值的符號,比如我們將圓周率3.12415926定義成名為PI的常量,使代碼更容易閱讀。而且常量是在編譯時就代入運算的(常量就是一個符號,編譯時編譯器就會將該符號替換成實際值),不會造成任何性能上的損失。但這一點也可能會造成一個版本問題,即假如未來修改了常量所代表的值,那么用到此常量的地方都要重新編譯(我個人認為這也是常量名稱的由來,我們應該將恆定不變的值定義為常量,以免后期改動時產生版本問題)。下面的示例也驗證了這一點,Test1和Test2方法內部的常量運算在編譯后,就已經運算完成。

從上面示例,我們還能看出一點:常量key和value編譯后是靜態成員,這是因為常量通常與類型關聯而不是與實例關聯,從邏輯上說,常量始終是靜態成員。但對於在方法內部定義的常量,由於作用域的限制,不可能有方法之外的地方引用到這個常量,所以在編譯后,常量被優化了。

字段

字段是一種數據成員,在OOP的設計中,字段通常是用來封裝一個類型的內部狀態,而方法表示的是對這些狀態的一些操作。

在C#中字段可用的修飾符有

  • Static 聲明的字段與類型關聯,而不是與對象關聯(默認情況下字段與對象關聯)
  • Readonly 聲明的字段只能在構造器里寫入值(可以通過反射修改)
  • Volatile 聲明的字段為易失字段(用於多線程環境)

這里要注意的是將一個字段標記為readonly時,不變的是引用,而不是引用的值。示例:

    class ReadonlyField
    {
        //chars 保存的是一個數組的引用
        public readonly char[] chars = new char[] { 'A', 'B', 'C' };

        void Main()
        {
            //以下改變數組內存,可以改成功
            chars[0] = 'X';
            chars[1] = 'Y';
            chars[2] = 'Z';

            //以下更改chars引用,無法通過編譯
            chars = new char[] { 'X', 'Y', 'Z' };
        }
    }

屬性

CLR支持兩種屬性:無參屬性和有參屬性(C#中稱為索引器)。

面向對象設計和編程的重要原則之一就是數據封裝,這意味着字段(封裝對象的內部狀態)永遠不應該公開。因此,CLR提供屬性機制來訪問字段內容(VS中輸入propfull加兩次Tab會為我們自動生成字段和屬性的代碼片斷)。

下面的示例中,Person對象內部有一個表示年齡的字段,如果直接公開這個字段,則不能保存外部不會將age設置為0或1000,這顯然是沒有意義的(也破壞了數據封裝性),所以通過屬性,可以在操作字段時,加一些額外邏輯,以保證數據的有效性。

 class Person
    {
        //Person對象的內部狀態
        private int age;

        //用屬性來安全地訪問字段
        public int Age
        {
            get { return age; }
            set
            {
                if (value > 0 && value <= 150) age = value;
                else { }    //拋出異常
            }
        }
    }

編譯上述代碼后,實際上編譯器會將屬性內的get和set訪問器各生成一個方法,方法名稱是get_和set_加上屬性名,所以說屬性的本質是方法

如果只是為了封裝一個字段而創建一個屬性,C#還為我們提供了一種更簡單的語法,稱為自動實現的屬性(AIP)。下面是一個示例(在VS中輸入prop加兩次TAB會為我們生成AIP片斷):

這里要注意一點,由於AIP的支持字段是編譯器自動生成的,而且編譯器每次編譯都可能更改這個名稱。所以在任何要序列化和反序列化的類型中,都不要使用AIP功能

對象和集合初始化器

在實現編程中,我們經常構造一個對象,然后設置對象的一些公共屬性或字段。為此C#為我們提供了一種簡化的語法來完成這些操作。如下示例:

    class Person
    {
        //AIP
        public string Name { get; set; }
        public int Id { get; set; }
        public int Age { get; set; }

        void Main()
        {
            //沒有使用對象初始化器的語法
            Person p1 = new Person();
            p1.Id = 1;
            p1.Name = "Heku";
            p1.Age = 24;

            //使用對象初始化器的語法
            Person p2 = new Person() { Id = 1, Name = "Heku", Age = 24 };
        }
  }

使用對象初始化器的語法時,實際上編譯器為我們生成的代碼和上面是一致的,但是下面的代碼明顯更加簡潔。如果本來就是要調用類型的無參構造器,C#還允許我們省略大括號之前的小括號:

Person p2 = new Person { Id = 1, Name = "Heku", Age = 24 };

如果一個屬性的類型實現了IEnumerable或IEnumerable<T>接口,那么這個屬性就被認為是一個集合,我們同樣類似的語法來初始化一個集合。比如我們在上例中的Person類中加入一個新屬性Skills

public List<string> Skills { get; set; }

然后可以用下面的語法來初始化

//使用簡化的對象初始化器語法+簡化集合初始化器語法
Person p3 = new Person { Id = 1, Name = "heku", Age = 24, Skills = new List<string> { "C#", "jQuery" } };

這里我們用new List<string> { "C#", "jQuery" }一句來初始化了一個集合(實現上new List<string>完全可以省略,編譯器會根據屬性的類型來自動推斷集合類型),並添加了兩項紀錄。編譯器會我們生成的代碼看起來是這樣的:

p3.Skills = new List<string>();
p3.Skills.Add("C#");
p3.Skills.Add("jQuery");

匿名類型

有時候,我們需要封裝一組數據,只有屬性或字段,沒有方法,並且只用於當前程序,不在項目間重用。

如果按傳統做法,我們需要手工定義一個類來封裝這組數據。匿名類型提供了一種方便的方法,可用來將一組只讀屬性封裝到單個對象中,而無需首先顯式定義一個類型。如下示例:

從上面示例可以看到,我們只寫了一句

var obj = new { Id = 1, Name = "Heku", Addr = "China" };

編譯器會為我們做大量的工作,首先為我們定義一個類型(類型名稱是編譯器編譯時才生成的,編程時還不知道,所以叫匿名類型),類型內部包含了三個屬性Id、Name、Addr以及對應的支持字段,同時也生成了三個get訪問器方法,沒有生成set訪問器方法(說明匿名類型的屬性是只讀的)。

另外在Main方法中,由於我們編程過程中,並不知道類型名稱,所以必須var關鍵字來讓編譯器自行推薦類型(雖然也可以用object或dynamic,但這完全沒有意義)。通過查看Main編譯后的IL代碼我們還可以發現,匿名類型的初始化是通過調用匿名類型的有參構造器來完成的,這點與之前也不相同(因為匿名類型屬性是只讀的,不能通過調用無參初始化器初始化后再設置屬性值,編譯器也根本沒有生成匿名類型的無參構造器)。

有參屬性

前面講到的屬性都沒有參數,實現上還有一種可以帶參數的屬性,稱之為有參屬性(C#中叫索引器)。

    class StringArray
    {
        private string[] array;

        public StringArray()
        {
            array = new string[10];
        }

        //有參屬性
        public string this[int index]
        {
            get
            {
                return array[index];
            }
            set
            {
                array[index] = value;
            }
        }



        void Main()
        {
            StringArray array = new StringArray();            
            array[0] = "Hello"; 
            array[1] = "World";

            string ss = array[0] + array[1];
        }
    }

上面的例子中,和定義無參屬性不同的是,這里並沒有屬性名稱,而是this[參數]的語法來定義一個有參屬性(索引器),這是C#的要求。和無參屬性不同,有參屬性還支持重載:

        //有參屬性
        public string this[int index]
        {
            get { return array[index]; }
            set { array[index] = value; }
        }

        //有參屬性重載
        public string this[int index, bool isStartFromEnd]
        {
            get
            {
                if (isStartFromEnd) return array[10 - index];
                else return array[index];
            }
            set
            {
                if (isStartFromEnd) array[10 - index] = value;
                else array[index] = value;
            }
        }

屬性本質是方法,有參屬性也一樣(對CLR來說甚至並不分有參還是無參,對它來說都是方法的調用),那么有參屬性的編譯后生成的IL是什么樣子呢?事實上C#對所有的有參屬性生成的IL方法都默認命名為get_Item和set_Item。當然這是可以通過在索引器上應用System.runtime.CompliserServices.IndexerNameAttribute定制Attribute來改變這一默認行為。

屬性訪問器的可訪問性

屬性的get和set訪問器是可以定義不同的訪問性的,如果get和set訪問器的可訪問性不一致,C#要求必須為屬性本身指定限制最小的那一個。

 protected string Name
 {
     get { return name; }    
     private set { name = value; }
 }

注意:如果同時設置get和set的訪問性,會提示“不能為屬性的兩個訪問器同時指定可訪問性修改符”,因為對屬性或索引器使用訪問修飾符受以下條件的制約:

  • 不能對接口或顯式接口成員實現使用訪問器修飾符
  • 僅當屬性或索引器同時具有 set 和 get 訪問器時,才能使用訪問器修飾符。這種情況下,只允許對其中一個訪問器使用修飾符
  • 如果屬性或索引器具有 override 修飾符,則訪問器修飾符必須與重寫的訪問器的訪問性(如果有的話)匹配
  • 訪問器的可訪問性級別必須比屬性或索引器本身的可訪問性級別具有更嚴格的限制

結尾的話

一邊搬書一邊敲代碼一邊寫博文,不知不覺又坐了將近一天(汗~我要出去活動一下了)。

能力水平有限,博文主要還是出於自我總結目的,如果有文中有誤,還請大家指出,感謝!

參考

1、CLR Via C#

2、非對稱訪問器可訪問性

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM