抽象類,是一種特殊的類,可以定義具有實現的方法,也可以定義未實現的方法契約,本身不能被實例化,只能在派生類中進行實例化。接口,對一組方法簽名進行統一的命名,只能定義未實現的方法契約,本身也不能被實例化,只能在實現類中進行實例化。二者都可以有部分數據成員(如:屬性),它們貌似有着相同的“契約”功能,但對各自的派生類(實現類)又有着不同的要求,那么,到底它們有何異同呢?這一章將從四個方面來講解它們的相同與不同之處。
抽象類 不能實例化。抽象類的用途是提供多個派生類可共享的基類的公共定義,是對類進行抽象,可以有實現,也可以不實現。使用關鍵字abstract進行定義。如下定義一個抽象類:
public abstract class Code_06_03 { }
再來看一下編譯器為它生成的IL:
.class public abstract auto ansi beforefieldinit ConsoleApp.Example06.Code_06_03 extends [mscorlib]System.Object { } // end of class ConsoleApp.Example06.Code_06_03
可以看以,抽象類實際上是繼承了System.Object類,並且編譯器為它生成了一個默認的構造函數。
接口 它是對一組方法簽名進行統一命名,是對一組行為規范的定義,各個行為(方法)之間相互疏。使用關鍵字interface進行定義,如下定義一個接口:
public interface ICode_06_01 { }
再來看一下編譯器的工作:
.class interface public abstract auto ansi ConsoleApp.Example06.ICode_06_01 { } // end of class ConsoleApp.Example06.ICode_06_01
可以看到,接口實際上是把它當成抽象類來看待,但是沒有構造函數。
無論是抽象類擁有構造函數,還是接口不擁有構造函數,它們都是不能被實例化的。
抽象類:描述
1) 可以定義抽象方法,抽象方法沒有具體實現,僅僅是一個方法的契約,在子類中重寫該方法。抽象類可以重寫父類的虛方法為抽象方法。
2) 可以定義非抽象方法,但要求該方法要有具體實現,如果該方法是虛方法,則在子類中可以重寫該方法。
3) 可以定義字段,屬性,抽象屬性,事件及靜態成員。
如下是對類Code_06_03的擴充:

public abstract class Code_06_03 { Dictionary<Guid, string> Root = new Dictionary<Guid, string>(); public string Sex { get; set; } public abstract string Address { get; } public abstract int Add(int a, int b); protected virtual string GetAddress(string addressID) { return addressID + " 北京"; } public void AddRoot(Guid id, string rootName) { this.Root.Add(id, rootName); OnAddRoot(); } public event EventHandler AddRootEvent; void OnAddRoot() { EventHandler handler = AddRootEvent; if (handler != null) { handler(this, null); } } public string this[Guid key] { get { return Root[key]; } set { Root[key] = value; } } }
抽象方法public abstract int Add(int a, int b);的IL:
.method public hidebysig newslot abstract virtual instance int32 Add(int32 a, int32 b) cil managed { } // end of method Code_06_03::Add
編譯器把Add方法當作一個虛方法,在子類中可以被重寫。
虛方法protected virtual string GetAddress(string addressID)的IL:
.method family hidebysig newslot virtual instance string GetAddress(string addressID) cil managed { //省略 }
它本來就是一個虛方法,所以編譯器並沒有特殊對待它。
方法public void AddRoot(Guid id, string rootName)的IL:
.method public hidebysig instance void AddRoot(valuetype [mscorlib]System.Guid id, string rootName) cil managed { //省略 }
也是一個普通的對象方法。
接口
1) 可以定義屬性及索引器,但不能定義字段。
2) 可以定義事件。
3) 可以定義方法,僅僅是方法簽名的約定,不得有實現,在實現類中對該方法進行具體實現,有點類似於抽象類的抽象方法。
4) 不可以定義虛方法。
5) 不可以定義任何靜態成員。
6) 接口成員默認是全開放的,不得有訪問修飾符。
如下,定義一個接口:
public interface ICode_06_01 { string Name { get; set; } int Add(int a, int b); event EventHandler AddEvent; }
方法int Add(int a, int b);的IL:
.method public hidebysig newslot abstract virtual instance int32 Add(int32 a, int32 b) cil managed { } // end of method ICode_06_01::Add
可以看到,定義的時候,我們並沒有為其指定可訪問修飾符(編譯器也不允許我們明文指定其可訪問修飾符),但編譯器默認將它的訪問級別指定為public。另外是把它當作一個抽象的虛方法。
至於成員屬性和事件,編譯器則將它們當作普通的對象屬性和對象事件對待,會為它們生成相應的get/set和add/remove 方法,並無特別之處。
抽象類的實現
由於抽象類也是類,所以對它的實現就像普通的繼承一樣,子類通過繼承可以得到抽象類的公有成員,且可以重寫部分成員,如虛方法和抽象方法等。如下是對Code_06_03類的實現:
public class Code_06_04 : Code_06_03 { public override int Add(int a, int b) { return a + b; } protected override string GetAddress(string addressID) { return "BeiJing"; } string _addressPrefix = "China "; public override string Address { get { return _addressPrefix; } } }
來看一下編譯器的工作:
可以看到類Code_06_04是標准、明白、徹底地對類Code_06_03的繼承。兩個重寫的方法Add和GetAddress都是普通的對象方法,只是依然被當作虛方法來看待,來看一下Add方法的IL:
.method public hidebysig virtual instance int32 Add(int32 a, int32 b) cil managed { //省略 }
方法GetAddress的IL:
.method family hidebysig virtual instance string GetAddress(string addressID) cil managed { //省略 }
因為這兩個方法保持着虛方法的特性,所以對於Code_06_04類的子類,同樣還可以重寫這兩個方法。
屬性成員Address這里還是一普通的對象屬性。
接口的實現
對接口的實現跟對抽象類的實現相似,如下是對接口ICode_06_01的實現類:

public class Code_06_02 : ICode_06_01 { string _name; public string Name { get { return _name; } set { _name = value; } } public int Add(int a, int b) { OnAdded(); return a + b; } public event EventHandler AddEvent; void OnAdded() { EventHandler handler = AddEvent; if (handler != null) { handler(this, null); } } }
來看一下編譯器的工作:
它與普通類的區別不大,只是很明確的是實現了接口ICode_06_01,來看一下它的IL:
.class public auto ansi beforefieldinit ConsoleApp.Example06.Code_06_02 extends [mscorlib]System.Object implements ConsoleApp.Example06.ICode_06_01 { } // end of class ConsoleApp.Example06.Code_06_02
可以看到,類Code_06_02不僅繼承於System.Object類,同時還實現了接口ICode_06_01。再來看一下對於接口中的方法,編譯器是如何處理的。Add.IL:
.method public hidebysig newslot virtual final instance int32 Add(int32 a, int32 b) cil managed { //省略 }
編譯器認為Add方法具有虛方法的特性。而對於屬性和事件,依然是普通的實現,如get/set、add/remove。
另外,接口還支持顯示實現接口,我們上面討論的Code_06_02類對接口的實現默認是隱式實現。
在接口的實現類內部,可以存在一個與接口某一方法名(包括簽名)完全相同的方法,但要求那個對接口實現的方法必須是顯示實現,如下代碼:
public int Add(int a, int b) { return a + b; } int ICode_06_01.Add(int a, int b) { OnAdded(); return a + b; }
可以看出顯示實現就是在方法前加上接口名的前綴和點號(ICode_06_01.),同時也可以看到,顯示實現接口的方法是不能有可訪問修飾符的,編譯器會對其進行private處理。那如何才能調用顯示實現的接口方法呢?可以將實現類的對象轉為一個接口變量,再調用該變量的相應方法。如下代碼:
Code_06_02 code0602 = new Code_06_02(); ICode_06_01 icode0602 = code0602; icode0602.Add(1, 2);
而對於抽象類的實現,是不能進行顯示實現的!
1) 抽象類保留一普通類的部分特性,定義可能已經實現的方法行為,方法內可以對數據成員(如屬性)進行操作,且方法可以相互溝通。而接口僅僅是定義方法的簽名,就像規則,只是約定,並沒有實現。
2) 抽象類的派生類可以原封不動地得到抽象類的部分成員,接口的實現類如果想要得到接口的數據成員,則必須對其進行重寫。
3) 一個類只能繼承於一個抽象(類),但可以實現多個接口,並且可以在繼承一個基類的基礎上,同時實現多個接口。
4) 抽象類和接口都不能對其使用密封sealed,事實上這兩者都是為了被其他類繼承和實現,對其使用sealed是沒有任何意義的。
5) 抽象類可以對接口進行實現。
6) 抽象類更多的用於“復制對象副本”,就是我們常說的“子類與父類有着is a的關系”,它更多關注於一個對象的整體特性。接口更多傾向於一系列的方法操作,這些操作在當前上下文中既有着相同作用對象,又相互隔離。
7) 某些時候,抽象類可以與接口互換
下面我們通過生活中見的紅娘拉線的示例,來說明抽象類與接口給我們變成帶來的方便性,下面代碼既可以使用抽象類也可以使用接口。
一般(這里說明的是一般性況下)只要想通過紅娘接線找到自己另一半的,在紅娘(Matchmaker)安排甲去見已以前,都會安排他/她(wooer)應該怎么跟對方溝通,加深對方對自己的好感印象,這些話是紅娘提供的,但說的動作,還是求婚者通過自己的“說話”本能來表達,只是紅娘在教求婚者說哪些套話而已。
一般我們會如下定義:

/// <summary> /// 紅娘 /// </summary> public class Matchmaker { string message; /// <summary> /// 指導求婚人如何表達 /// </summary> public void Teach() { message = "從見你的第一眼起,我就認為你會是那個每天早上跟我一起下床的人。但這絕對不是一見鍾情,那是什么?我會在我們老去的那一天告訴你。"; Wooer wooer = new Wooer(); wooer.Say(message); } } /// <summary> /// 求婚人 /// </summary> public class Wooer { /// <summary> /// 向對方表達 /// </summary> /// <param name="message"></param> public void Say(string message) { Console.WriteLine(message); } }
客戶程序:
public class ClientApp { public void Work() { Matchmaker matchmaker = new Matchmaker(); matchmaker.Teach(); } }
我們通常都會這么寫,簡單明了,實現功能沒問題。
紅娘在想:老娘一個人怎么能應付得了你們這一群剩男剩女!個個都有特殊情況,我哪里真有分身術!干脆我搭建一個平台,你們按我的要求填表,按我的步驟走就行了。
然而這里的紅娘Matchmaker是強依賴Wooer。因為畢竟無論求婚人是男是女,不僅要口頭語言表達,還有自己獨特的肢體語言表達,比如男人會哼哼嗨嗨地表達,女人會哼哼唧唧地表達,Wooer要變了,那這個時候Matchmaker就得變。這個要求就是把所有求婚人抽象出來,下面我們引入接口,將Wooer抽象出來為一個接口IWooer,讓Matchmaker依賴於IWooer,而不直接依賴於具體求婚人。接下來如下改造代碼:

/// <summary> /// 紅娘 /// </summary> public class Matchmaker { string message; /// <summary> /// 指導求婚人如何表達 /// </summary> public void Teach(IWooer wooer) { message = "從見你的第一眼起,我就認為你會是那個每天早上跟我一起下床的人。但這絕對不是一見鍾情,那是什么?我會在我們老去的那一天告訴你。"; wooer.Say(message); } } /// <summary> /// 求婚人 /// </summary> public interface IWooer { /// <summary> /// 求婚人自己准備的語言,有自己的真家伙拿出來實戰! /// </summary> string Message { get; } /// <summary> /// 肢體表達 /// </summary> void Action(); /// <summary> /// 語言表達 /// </summary> /// <param name="message"></param> void Say(string message); } public class ManWooer : IWooer { public string Message { get { return "我擁有制造印超機的技術"; } } public void Action() { //抱着對方、動手動腳、眼神像彎尺 //這些動作無法用代碼表達,你懂的,如餓狼一樣。。。 } public void Say(string message) { //先動起來預熱 this.Action(); //接着再表達 Console.WriteLine(message + this.Message); } } public class WomanWooer : IWooer { public string Message { get { return "我擁有1+2個點"; } } public void Action() { //側身向着對方、朦朧的眼神含情默默、相見恨晚,總是想看一眼對方,但害羞的臉蛋紅紅 //這些動作無法用代碼表達,你懂的,如羔羊一樣。。。 } public void Say(string message) { //先動起來預熱 this.Action(); //接着再表達 Console.WriteLine(message + this.Message); } } 再來看看客戶程序: public class ClientApp { public void Work() { Matchmaker matchmaker = new Matchmaker(); //matchmaker.Teach(); IWooer wooer; //求婚者是男人 wooer = new ManWooer(); matchmaker.Teach(wooer); //求婚者是女人 wooer = new ManWooer(); matchmaker.Teach(wooer); matchmaker.Teach(wooer); } }
很明顯,現在Matchmaker已經不再直接依賴Wooer了,只依賴第三方IWooer,不論來的是男人還是女人,只要你按照紅娘要求的步驟就行了,如此開放的平台,說不定明年就能創業板闖關成功!