967
汽車大全
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");
有參屬性
前麵講到的屬性都沒有參數,實現上還有一種可以帶參數的屬性,稱之為有參屬性(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 修飾符,則訪問器修飾符必須與重寫的訪問器的訪問性(如果有的話)匹配
- 訪問器的可訪問性級別必須比屬性或索引器本身的可訪問性級別具有更嚴格的限製
- 屬性沒有存儲數據的功能,數據都存在字段中,所以隻有修改字段的數據才能更改數據,修改屬性的值沒用。
- 原文地址
最後更新:2017-04-03 12:54:12