首先吐糟一下今天杭州的天氣,真是太熱了!雖然沒有妹子跟我約會,但宅在方寸大的窩裡,也是煩躁不已!
接上一篇《C#基礎之基本類型》
類型和成員基礎
在C#中,一個類型內部可以定義多種成員:常量、欄位、執行個體構造器、類型構造器(靜態構造器)、方法、操作符重載、轉換操作符、屬性、事件、類型。
類型的可見度有public和internal(預設)兩種,前者定義的類型對所有程式集中的所有類型都可見,後者定義的類型只對同一程式集內部的所有類型可見:
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、非對稱訪問器可訪問性