類 就像自然界的事物一樣,擁有反應其自身狀態特性的一系列數據。類狀態數據是由常量、字段、屬性等一些基礎成員組成,且有靜態和實例之分。它們之間有什么區別呢?可以互相替代嗎?常量與靜態只讀字段有什么區別呢?屬性是用方法實現,那么實現它的方法可以有參數嗎?本章將解釋這些奧秘。
常量是一個符號,是在編譯時已經存在且在程序生命周期內不會發生改變的值,它被保存在程序集的元數據中,只能使用C#內置的數據類型(基元類型)定義,如:int、uint、long 等,當然不包括System.Object。既然是內置類型定義,它必然是在聲明時同時已初始化。常量使用const定義,C#編譯器總是默認為static成員,且不可明文指定其訪問修飾符為static。常量的可訪問修飾符為:public、private、protected、internal 或 protected internal。如下代碼:
public class Code_03 { public const double PAI = 3.14; public void Test() { double area = PAI * Math.Sqrt(20); } }
IL如圖:
通過IL我們可以看到,常量中定義的PAI值3.14(3.1400000000000001)實際是直接被編譯到目標代碼的元數據中的,而不是對PAI的引用。
如此一來,就引出一個問題:常量PI定義在程序集A中,如果程序集B在使用A中的PAI時,經過編譯其真實值是直接被編譯到B的IL中,當下一次我們要對PAI的值進行更改時(比如把3.14改為3.1415926),如果只編譯程序集A而不編譯程序集B,則會導致程序集B得不到更新,還保留原來的3.14,必須對程序集B重新編譯才能獲取新值3.1415926。要想解決此問題,可以使用接下來我們要討論的字段,給字段加上readonly修飾符不僅可以達到與常量同樣的目的:程序運行期間其值不可更改,而且可以避免每次重新編譯程序集B。
字段是構成類結構的一種元素,它不僅可以用C#內置類型進行聲明,也可以用任意的自定義類型進行聲明,很明顯,它不僅可以保存一個值類型的實例,也可以保存一個引用類型的地址引用。字段可以直接定義在類或結構中。相比常量,它就多了一些特性,它不僅可以是類的狀態數據,也可以是實例的狀態數據,它默認並不是static,而是對象級的成員,除非明確指定其修飾符為static。字段可以使用的修飾符為: public、private、protected、internal 或 protected internal。另外,readonly也可以用於字段,如果再加上satic,此時它就相當於常量了,只不過對象級的字段初始化是在構造函數中進行的,類級的字段初始化是在靜態構造函數中進行的。如下代碼:
public class Code_03 { public const double PAI = 3.14; double radius = 20; static int a = 10; static readonly int b = 30; }
可以看到,編譯器自動生成了一個靜態構造函數,並在其內對a和b進行初始化,另外在實例級構造函數內對radius進行初始化。需要說明一點的是:如果人為的要在構造函數中對常量PAI進行更改,在編譯器檢查語法過程中將會報錯,編譯器不允許這樣操作。
常量是在編譯時計算,字段是在運行時字段,並且常量與static readonly字段有着相同之處,那它們又有區別與聯系呢?
const常量在聲明處進行初始化,編譯時直接將值編譯進元數據,運行時不能進行值更改(如下面代碼中的PAI)。
實例字段可在定義處和構造函數內進行初始化。可在任意處進行更改,如果其可訪問性允許(如下面代碼中的radius)。
static 字段聲明為類級的字段,它屬於類的狀態數據。可在任意處進行更改,如果其可訪問性允許(如下面代碼中的a)。
readonly 字段聲明只讀字段,只能在構造函數內對其進行更改(如下面代碼中的b)。
static readonly 字段聲明靜態只讀字段,它屬於類級且只讀。只能在靜態構造函數內對其更改(如下面代碼中的c)。
示例代碼:

public class Code_03 { public const double PAI = 3.14; double radius = 20; static int a = 10; readonly int b = 0; static readonly int c = 30; static Code_03() { a = 100; c = 1000; //錯誤 非靜態的字段、方法或屬性“ConsoleApp.Example03.Code_03.c”要求對象引用 //radius = 1; //b = -1; } public Code_03() { radius = 1; a = -1; b = -1; //錯誤 無法對靜態只讀字段賦值(靜態構造函數或變量初始值中除外) //c = -1; } public void MyMethod() { radius = 1; a = -1; //錯誤 無法對靜態只讀字段賦值(靜態構造函數或變量初始值中除外) //b = -1; //c = -1; } }
字段通常保存着類或對象本身的狀態,我們當然可以將其公開為public讓外界對其進行讀、寫修改。從某種意義上來講,我們更希望在類本身內部對自己的狀態進行維護,並不希望外界對自己的狀態進行直接更改,以防止破壞這些數據,所幸的是還有一個數據成員可供使用,它就是屬性。
如果在外部要訪問某一個類的內部成員(私有字段),可以使用方法來達到目的,但如果對每一個字段都去編寫一個方法來進行讀寫操作似乎又麻煩了些。屬性以靈活的方式實現了對私有字段的訪問,它是一種“訪問器”方法,包括get方法和set方法,更明確地說,屬性就是方法的精簡寫法的實現,隱藏了實現和驗證的代碼。它有兩個訪問器:
get訪問器用於獲取屬性的值。
set訪問器用於設定屬性的值。既然它是方法,且是要在方法內對私有字段用新值進行更改替換,那么它就是可以(或者說是應該)接收參數的, value 關鍵字就是用於定義由 set 取值函數分配的值。假如有如下一個屬性的定義:
public class Code_03_2 { string _name; public string Name { get { return _name; } set { _name = value; } } }
這個定義是通過屬性Name對私有字段_name進行訪問,我們來看一下編譯器對IL做了哪些處理:
編譯器自動生成了get_和set_方法。其中方法get_name()的IL如下:
.method public hidebysig specialname instance string get_Name() cil managed { // 代碼大小 12 (0xc) .maxstack 1 .locals init ([0] string CS$1$0000) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld string ConsoleApp.Example03.Code_03_2::_name IL_0007: stloc.0 IL_0008: br.s IL_000a IL_000a: ldloc.0 IL_000b: ret } // end of method Code_03_2::get_Name
它是在方法內部讀取字段_name然后返回。
set_Name(string)方法如下:
.method public hidebysig specialname instance void set_Name(string 'value') cil managed { // 代碼大小 9 (0x9) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 IL_0002: ldarg.1 IL_0003: stfld string ConsoleApp.Example03.Code_03_2::_name IL_0008: ret } // end of method Code_03_2::set_Name
它是在方法內部對字段_name進行值修改。
我們已經看出來,屬性是方法的實現,既然是方法,那方法就可以被訪問修飾符定義,如public、private等,來限定方法的可訪問性,很顯然,我們也可以對屬性的可訪問性進行限定,這里是對訪問器set和get進行訪問限定的。比如:
string _name; public string Name { get { return _name; } private set { _name = value; } }
如此一來,則此屬性Name是只讀屬性,如果願意,也可以將其定義為只寫屬性,但這樣做好像沒什么意思。
另外,屬性還有一種更簡潔的寫法如下:
public int Age { get; set; } public string Address { get; set; }
在編譯的時候,編譯器會自動生成對應的私有字段_age和_address,同樣也會生成相應的get_方法和set_方法。
還有一種數據結構與屬性很類似,稱為索引器,它同樣有get和set訪問器,只是它的get方法接受大於或等於一個參數,它的set方法接受大於或等於兩個參數。通常它在類內部維護一個集合,如Array、List等。如下代碼:
public class Code_03_3 { string[] _nameList = new string[100]; public string this[int i] { get { return _nameList[i]; } set { _nameList[i] = value; } } }
this關鍵字用於定義索引器,value 關鍵字用於定義由 set 索引器分配的值。再來看一下編譯器都干了什么事?
編譯器自動生成了兩個方法get_Item(int32)和set_Item(int32,string),它們接受了大於等於一個參數。最后要說明一點的是:索引器不必根據整數值進行索引,也可以用其他類型進行索引。如下的定義是用Guid進行索引:
Dictionary<Guid, string> data = new Dictionary<Guid, string>(); public string this[Guid key] { get { return data[key]; } set { data[key] = value; } }
我們一直在討論屬性是封裝了對類內部私有字段成員的訪問,它提供了一種書寫更簡便、訪問控制更安全實現方式。根據建議,我們應該盡量避免在屬性的訪問器內進行過多的邏輯運算,如果確實有復雜的邏輯運行,請考慮使用方法,我們將在下一章討論方法的方方面面。