c#銳利體驗
第四講 類與對象[/b]
南京郵電學院 李建忠([email protected])
組件編程不是對傳統面向對象的拋棄,相反組件編程正是面向對象編程的深化和發展。類作為面向對象的靈魂在c#語言里有著相當廣泛深入的應用,很多非?!皊harp”的組件特性甚至都是直接由類包裝而成。對類的深度掌握自然是我們“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()調用,系統拋出空引用錯誤)。從這里我們也可看出設計靜態方法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”特技堪為浩瀚,讓我們繼續期待下面的專題!