教程 C#銳利體驗
第四講 類與對象[/b]
南京郵電學院 李建忠(cornyfield@263.net)
組件編程不是對傳統物件導向的拋棄,相反組件編程正是物件導向編程的深化和發展。類作為物件導向的靈魂在C#語言裡有著相當廣泛深入的應用,很多非常“Sharp”的組件特性甚至都是直接由類封裝而成。對類的深度掌握自然是我們“Sharp XP”重要的一環。
類
C#的類是一種對包括資料成員,函數成員和巢狀型別進行封裝的資料結構。其中資料成員可以是常量,域。函數成員可以是方法,屬性,索引器,事件,操作符,執行個體構建器,靜態構建器,析構器。我們將在“第五講 構造器與析構器”和“第六講 域 方法 屬性與索引器”對這些成員及其特性作詳細的剖析。除了某些匯入的外部方法,類及其成員在C#中的聲明和實現通常要放在一起。
C#用多種修飾符來表達類的不同性質。根據其保護級C#的類有五種不同的限制修飾符:
- public可以被任意存取;
- protected只可以被本類和其繼承子類存取;
- internal只可以被本組合體(Assembly)內所有的類存取,組合體是C#語言中類被組合後的邏輯單位和物理單位,其編譯後的副檔名往往是“.DLL”或“.EXE”。
- protected internal唯一的一種組合限制修飾符,它只可以被本組合體內所有的類和這些類的繼承子類所存取。
- private只可以被本類所存取。
如果不是嵌套的類,命名空間或編譯單元內的類只有public和internal兩種修飾。
new修飾符只能用於嵌套的類,表示對繼承父類同名類型的隱藏。
abstract用來修飾抽象類別,表示該類只能作為父類被用於繼承,而不能進行對象執行個體化。抽象類別可以包含抽象的成員,但這並非必須。abstract不能和new同時用。下面是抽象類別用法的偽碼:
abstract class A{ public abstract void F();}abstract class B: A{ public void G() {}}class C: B{ public override void F() { //方法F的實現 }}
抽象類別A內含一個抽象方法F(),它不能被執行個體化。類B繼承自類A,其內包含了一個執行個體方法G(),但並沒有實現抽象方法F(),所以仍然必須聲明為抽象類別。類C繼承自類B,實作類別抽象方法F(),於是可以進行對象執行個體化。
sealed用來修飾類為密封類,阻止該類被繼承。同時對一個類作abstract和sealed的修飾是沒有意義的,也是被禁止的。
對象與this關鍵字
類與對象的區分對我們把握OO編程至關重要。我們說類是對其成員的一種封裝,但類的封裝設計僅僅是我們編程的第一步,對類進行對象執行個體化,並在其資料成員上實施操作才是我們完成現實任務的根本。執行個體化對象採用MyClass myObject=new MyClass()文法,這裡的new語義將調用相應的構建器。C#所有的對象都將建立在託管堆上。執行個體化後的類型我們稱之為對象,其核心特徵便是擁有了一份自己特有的資料成員拷貝。這些為特有的對象所持有的資料成員我們稱之為執行個體成員。相反那些不為特有的對象所持有的資料成員我們稱之為靜態成員,在類中用static修飾符聲明。僅對待用資料成員實施操作的稱為靜態函數成員。C#中待用資料成員和函數成員只能通過類名引用擷取,看下面的代碼:
using System;class A{ public int count; public void F() { Console.WriteLine(this.count); } public static string name; public static void G() { Console.WriteLine(name); }}class Test{ public static void Main() { A a1=new A(); A a2=new A(); a1.F(); a1.count=1; a2.F(); a2.count=2; A.name="CCW"; A.G(); }}
我們聲明了兩個A對象a1,a2。對於執行個體成員count和F(),我們只能通過a1,a2引用。對於靜態成員name和G()我們只能通過類型A來引用,而不可以這樣a1.name,或a1.G()。
在上面的程式中,我們看到在執行個體方法F()中我們才用this來引用變數count。這裡的this是什麼意思呢?this 關鍵字引用當前對象執行個體的成員。在執行個體方法體內我們也可以省略this,直接引用count,實際上兩者的語義相同。理所當然的,靜態成員函數沒有 this 指標。this 關鍵字一般用於從建構函式、執行個體方法和執行個體訪問器中訪問成員。
在建構函式中this用於限定被相同的名稱隱藏的成員,例如:
class Employee{public Employee(string name, string alias) { this.name = name; this.alias = alias; }}
將對象作為參數傳遞到其他方法時也要用this表達,例如:
CalcTax(this);
聲明索引器時this更是不可或缺,例如:
public int this [int param]{ get { return array[param]; } set { array[param] = value; }}
System.Object類
C#中所有的類都直接或間接繼承自System.Object類,這使得C#中的類得以單根繼承。如果我們沒有明確指定繼承類,編譯器預設認為該類繼承自System.Object類。System.Object類也可用小寫object關鍵字表示,兩者完全等同。自然C#中所有的類都繼承了System.Object類的公用介面,剖析它們對我們理解並掌握C#中類的行為非常重要。下面是僅用介面形式表示的System.Object類:
namespace System{ public class Object { public static bool Equals(object objA,object objB){} public static bool ReferenceEquals(object objA,object objB){} public Object(){} public virtual bool Equals(object obj){} public virtual int GetHashCode(){} public Type GetType(){} public virtual string ToString(){} protected virtual void Finalize(){} protected object MemberwiseClone(){} }
我們先看object的兩個靜態方法Equals(object objA,object objB),ReferenceEquals(object objA,object objB)和一個執行個體方法Equals(object obj)。在我們闡述這兩個方法之前我們首先要清楚物件導向編程兩個重要的相等概念:值相等和引用相等。值相等的意思是它們的資料成員按記憶體位分別相等。引用相等則是指它們指向同一個記憶體位址,或者說它們的物件控點相等。引用相等必然推出值相等。對於實值型別關係等號“= =”判斷兩者是否值相等(結構類型和枚舉類型沒有定義關係等號“= =”,我們必須自己定義)。對於參考型別關係等號“= =”判斷兩者是否引用相等。實值型別在C#裡通常沒有引用相等的表示,只有在非託管編程中採用取地址符“&”來間接判斷二者的地址是否相等。
靜態方法Equals(object objA,object objB)首先檢查兩個對象objA和objB是否都為null,如果是則返回true,否則進行objA.Equals(objB)調用並返回其值。問題歸結到執行個體方法Equals(object obj)。該方法預設的實現其實就是{return this= =obj;}也就是判斷兩個對象是否引用相等。但我們注意到該方法是一個虛方法,C#推薦我們重寫此方法來判斷兩個對象是否值相等。實際上Microsoft.NET架構類庫內提供的許多類型都重寫了該方法,如:System.String(string),System.Int32(int)等,但也有些類型並沒有重寫該方法如:System.Array等,我們在使用時一定要注意。對於參考型別,如果沒有重寫執行個體方法Equals(object obj),我們對它的調用相當於this= =obj,即引用相等判斷。所有的實值型別(隱含繼承自System.ValueType類)都重寫了執行個體方法Equals(object obj)來判斷是否值相等。
注意對於對象x,x.Equals(null)返回false,這裡x顯然不能為null(否則不能完成Equals()調用,系統拋出Null 參考錯誤)。從這裡我們也可看出設計靜態方法Equals(object objA,object objB)的原因了--如果兩個對象objA和objB都可能為null,我們便只能用object. Equals(object objA,object objB)來判斷它們是否值相等了--當然如果我們沒有改寫執行個體方法Equals(object obj),我們得到的仍是引用相等的結果。我們可以實現介面IComparable(有關介面我們將在“第七講 介面 繼承與多態”裡闡述)來強制改寫執行個體方法Equals(object obj)。
對於實值型別,執行個體方法Equals(object obj)應該和關係等號“= =”的傳回值一致,也就是說如果我們重寫了執行個體方法Equals(object obj),我們也應該重載或定義關係等號“= =”操作符,反之亦然。雖然實值型別(繼承自System.ValueType類)都重寫了執行個體方法Equals(object obj),但C#推薦我們重寫自己的實值型別的執行個體方法Equals(object obj),因為系統的System.ValueType類重寫的很低效。對於參考型別我們應該重寫執行個體方法Equals(object obj)來表達值相等,一般不應該重載關係等號“= =”操作符,因為它的預設語義是判斷引用相等。
靜態方法ReferenceEquals(object objA,object objB)判斷兩個對象是否引用相等。如果兩個對象為參考型別,那麼它的語義和沒有重載的關係等號“= =”操作符相同。如果兩個對象為實值型別,那麼它的傳回值一定是false。
執行個體方法GetHashCode()為相應的類型提供雜湊(hash)碼值,應用於雜湊演算法或雜湊表中。需要注意的是如果我們重寫了某類型的執行個體方法Equals(object obj),我們也應該重寫執行個體方法GetHashCode()--這理所應當,兩個對象的值相等,它們的雜湊碼也應該相等。下面的代碼是對前面幾個方法的一個很好的樣本:
using System;struct A{ public int count;}class B{ public int number;}class C{ public int integer=0; public override bool Equals(object obj) { C c=obj as C; if (c!=null) return this.integer==c.integer; else return false; } public override int GetHashCode() { return 2^integer; }}class Test{ public static void Main() { A a1,a2; a1.count=10; a2=a1; //Console.Write(a1==a2);沒有定義“= =”操作符 Console.Write(a1.Equals(a2));//True Console.WriteLine(object.ReferenceEquals(a1,a2));//False B b1=new B(); B b2=new B(); b1.number=10; b2.number=10; Console.Write(b1==b2);//False Console.Write(b1.Equals(b2));//False Console.WriteLine(object.ReferenceEquals(b1,b2));//False b2=b1; Console.Write(b1==b2);//True Console.Write(b1.Equals(b2));//True Console.WriteLine(object.ReferenceEquals(b1,b2));//True C c1=new C(); C c2=new C(); c1.integer=10; c2.integer=10; Console.Write(c1==c2);//False Console.Write(c1.Equals(c2));//True Console.WriteLine(object.ReferenceEquals(c1,c2));//False c2=c1; Console.Write(c1==c2);//True Console.Write(c1.Equals(c2));//True Console.WriteLine(object.ReferenceEquals(c1,c2));//True }}
如我們所期望,編譯器並運行我們會得到以下輸出:
TrueFalse
FalseFalseFalse
TrueTrueTrue
FalseTrueFalse
TrueTrueTrue
執行個體方法GetType()與typeof的語義相同,它們都通過查詢對象的中繼資料來確定對象的運行時類型,我們在“第十講 特徵與映射”對此作詳細的闡述。
執行個體方法ToString()返回對象的字串表達形式。如果我們沒有重寫該方法,系統一般將類型名作為字串返回。
受保護的Finalize()方法在C#中有特殊的語義,我們將在“第五講 構造器與析構器”裡詳細闡述。
受保護的MemberwiseClone()方法返回目前對象的一個“影子拷貝”,該方法不能被子類重寫。“影子拷貝”僅僅是對象的一份按位拷貝,其含義是對對象內的實值型別變數進行賦值拷貝,對其內的參考型別變數進行控制代碼拷貝,也就是拷貝後的引用變數將持有對同一塊記憶體的引用。相對於“影子拷貝”的是深度拷貝,它對參考型別的變數進行的是值複製,而非控制代碼複製。例如X是一個含有對象A,B引用的對象,而對象A又含有對象M的引用。Y是X的一個“影子拷貝”。那麼Y將擁有同樣的A,B的引用。但對於X的一個“深度拷貝”Z來說,它將擁有對象C和D的引用,以及一個間接的對象N的引用,其中C是A的一份拷貝,D是B的一份拷貝,N是M的一份拷貝。深度拷貝在C#裡通過實現ICloneable介面(提供Clone()方法)來完成。
對對象和System.Object的把握為類的學習作了一個很好的鋪墊,但這僅僅是我們銳利之行的一小步,關乎對象成員初始化,記憶體引用的釋放,繼承與多態,異常處理等等諸多“Sharp”特技堪為浩瀚,讓我們繼續期待下面的專題!