域
域(Field)又稱成員變數(Member Variable),它表示儲存位置,是C#中類不可缺少的一部分。域的類型可以是C#中任何資料類型。但對於除去string類型的其他參考型別由於在初始化時涉及到一些類的構造器的操作,我們這裡將不提及,我們把這一部分內容作為“類的嵌套”放在“介面 繼承與多態”一講內來闡述。
域分為執行個體域和靜態域。執行個體域屬於具體的對象,為特定的對象所專有。靜態域屬於類,為所有對象所共用。C#嚴格規定執行個體域只能通過對象來擷取,靜態域只能通過類來擷取。例如我們有一個類型為MyClass的對象MyObject,MyClass內的執行個體域instanceField(存取限制為public)只能這樣擷取:MyObject. instanceField。而MyClass的靜態域staticField(存取限制為public)只能這樣擷取:MyClass.staticField。注意靜態域不能像傳統C++那樣通過對象擷取,也就是說MyObject.staticField的用法是錯誤的,不能通過編譯器編譯。
域的存取限制集中體現了物件導向編程的封裝原則。如前所述,C#中的存取限制修飾符有5種,這5種對域都適用。C#只是用internal擴充了C++原來的friend修飾符。在有必要使兩個類的某些域互相可見時,我們將這些類的域聲明為internal,然後將它們放在一個組合體內編譯即可。如果需要對它們的繼承子類也可見的話,聲明為protected internal即可。實際上這也是組合體的本來意思--將邏輯相關的類組合封裝在一起。
C#引入了readonly修飾符來表示唯讀域,const來表示不變常量。顧名思義對唯讀域不能進行寫操作,不變常量不能被修改,這兩者到底有什麼區別呢?唯讀域只能在初始化--聲明初始化或構造器初始化--的過程中賦值,其他地方不能進行對唯讀域的賦值操作,否則編譯器會報錯。唯讀域可以是執行個體域也可以是靜態域。唯讀域的類型可以是C#語言的任何類型。但const修飾的常量必須在聲明的同時賦值,而且要求編譯器能夠在編譯時間期計算出這個確定的值。const修飾的常量為靜態變數,不能夠為對象所擷取。const修飾的值的類型也有限制,它只能為下列類型之一(或能夠轉換為下列類型的):sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, enum類型, 或參考型別。值得注意的是這裡的參考型別,由於除去string類型外,所有的類型出去null值以外在編譯時間期都不能由編譯器計算出他們的確切的值,所以我們能夠聲明為const的參考型別只能為string或值為null的其他參考型別。顯然當我們聲明一個null的常量時,我們已經失去了聲明的意義--這也可以說是C#設計的尷尬之處!
這就是說,當我們需要一個const的常量時,但它的類型又限制了它不能在編譯時間期被計算出確定的值來,我們可採取將之聲明為static readonly來解決。但兩者之間還是有一點細微的差別的。看下面的兩個不同的檔案:
//file1.cs//csc /t:library file1.csusing System;namespace MyNamespace1{public class MyClass1{public static readonly int myField = 10;}}//file2.cs//csc /r:file1.dll file2.csusing System;namespace MyNamespace2{public class MyClass1{public static void Main(){Console.WriteLine(MyNamespace1.MyClass1.myField);}}}
我們的兩個類分屬於兩個檔案file1.cs 和file2.cs,並分開編譯。在檔案file1.cs內的域myField聲明為static readonly時,如果我們由於某種需要改變了myField的值為20,我們只需重新編譯檔案file1.cs為file1.dll,在執行file2.exe時我們會得到20。但如果我們將static readonly改變為const後,再改變myField的初始化值時,我們必須重新編譯所有引用到file1.dll的檔案,否則我們引用的MyNamespace1.MyClass1.myField將不會如我們所願而改變。這在大的系統開發過程中尤其需要注意。實際上,如果我們能夠理解const修飾的常量是在編譯時間便被計算出確定的值,並代換到引用該常量的每一個地方,而readonly時在運行時才確定的量--只是在初始化後我們不希望它的值再改變,我們便能理解C#設計者們的良苦用心,我們才能徹底把握const和readonly的行為!
域的初始化是物件導向編程中一個需要特別注意的問題。C#編譯器預設將每一個域初始化為它的預設值。簡單的說,數實值型別(枚舉類型)的預設值為0或0.0。字元類型的預設值為'\x0000'。布爾類型的預設值為false。參考型別的預設值為null。結構類型的預設值為其內的所有類型都取其相應的預設值。雖然C#編譯器為每個類型都設定了預設類型,但作為物件導向的設計原則,我們還是需要對變數進行正確的初始化。實際上這也是C#推薦的做法,沒有對域進行初始化會導致編譯器發出警告資訊。C#中對域進行初始化有兩個地方--聲明的同時進行初始化和在構造器內進行初始化。如前所述,域的聲明初始化實際上被編譯器作為指派陳述式放在了構造器的內部的最開始處執行。執行個體變數初始化會被放在執行個體構造器內,靜態變數初始化會被放在靜態構造器內。如果我們聲明了一個靜態變數並同時對之進行了初始化,那麼編譯器將為我們構造出一個靜態構造器來把這個初始化語句變成指派陳述式放在裡面。而作為const修飾的常量域,從嚴格意義上講不能算作初始化語句,我們可以將它看作類似於C++中的宏代換。
屬性
屬性可以說是C#語言的一個創新。當然你也可以說不是。不是的原因是它背後的實現實際上還是兩個函數--一個賦值函數(get),一個存取子(set),這從它產生的中繼語言代碼可以清晰地看到。是的原因是它的的確確在語言層面實現了物件導向編程一直以來對“屬性”這一OO風格的類的特殊介面的訴求。理解屬性的設計初衷是我們用好屬性這一工具的根本。C#不提倡將域的保護層級設為public而使使用者在類外任意操作--那樣太不OO,或者具體點說太不安全!對所有有必要在類外可見的域,C#推薦採用屬性來表達。屬性不表示儲存位置,這是屬性和域的根本性的區別。下面是一個典型的屬性設計:
using System;class MyClass{int integer;public int Integer{get {return integer;}set {integer=value;}}}class Test{public static void Main(){MyClass MyObject=new MyClass();Console.Write(MyObject.Integer);MyObject.Integer++;Console.Write(MyObject.Integer);}}
一如我們期待的那樣,程式輸出0 1。我們可以看到屬性通過對方法的封裝向程式員提供了一個友好的域成員的存取介面。這裡的value是C#的關鍵字,是我們進行屬性操作時的set的隱含參數,也就是我們在執行屬性寫操作時的右值。
屬性提供了唯讀(get),唯寫(set),讀寫(get和 set)三種介面操作。對域的這三種操作,我們必須在同一個屬性名稱下聲明,而不可以將它們分離,看下面的實現:
class MyClass{private string name;public string Name { get { return name; }}public string Name { set { name = value; }}}
上面這種分離Name屬性實現的方法是錯誤的!我們應該像前面的例子一樣將他們放在一起。值得注意的是三種屬性(唯讀,唯寫,讀寫)被C#認為是同一個屬性名稱,看下面的例子:
class MyClass{protected int num=0;public int Num{set {num=value;}}}class MyClassDerived: MyClass{new public int Num{get {return num;}}}class Test{public static void Main(){MyClassDerived MyObject = new MyClassDerived();//MyObject.Num= 1; //錯誤 ! ((MyClass)MyObject).Num = 1; }}
我們可以看到MyClassDerived中的屬性Num-get{}屏蔽了MyClass中屬性Num-set{}的定義。
當然屬性遠遠不止僅僅限於域的介面操作,屬性的本質還是方法,我們可以根據程式邏輯在屬性的提取或賦值時進行某些檢查,警告等額外操作,看下面的例子:
class MyClass{private string name;public string Name { get { return name; }set {if (value==null)name="Microsoft";elsename=value;}}}
由於屬性的方法的本質,屬性當然也有方法的種種修飾。屬性也有5種存取修飾符,但屬性的存取修飾往往為public,否則我們也就失去了屬性作為類的公用介面的意義。除了方法的多參數帶來的方法重載等特性屬性不具備外, virtual, sealed, override, abstract等修飾符對屬性與方法同樣的行為,但由於屬性在本質上被實現為兩個方法,它的某些行為需要我們注意。看下面的例子:
abstract class A{int y;public virtual int X {get { return 0; }}public virtual int Y {get { return y; }set { y = value; }}public abstract int Z { get; set; }}class B: A{int z;public override int X {get { return base.X + 1; }}public override int Y {set { base.Y = value < 0? 0: value; }}public override int Z {get { return z; }set { z = value; }}}
這個例子集中地展示了屬性在繼承上下文中的某些典型行為。這裡,類A由於抽象屬性Z的存在而必須聲明為abstract。子類B中通過base關鍵字來引用父類A的屬性。類B中可以只通過Y-set便覆蓋了類A中的虛屬性。
靜態屬性和靜態方法一樣只能存取類的靜態域變數。我們也可以像做外部方法那樣,聲明外部屬性。