接口是一種按照契約設計的方式,一個類型必須實行接口中定義的方法。抽象基類則為一組相關的類型提供了一個共有的抽象。要注意二者的使用場景和區別:基類描述了對象是什么;接口描述了對象將如何表現行為。
1.關於接口
接口描述了一組功能,是一個契約,任何實現接口的類型必須為接口中定義的所有所有元素提供具體的實現。我們應該將可重用的行為提取出來,定義在接口中;由於不同相關的類型均可以實現一個接口,所有這會增加代碼的重用率。對於開發者本身來說,實現接口要比繼承自定義的基類更容易。
2.關於抽象基類
抽象基類除了描述共同行為,抽象基類還可以為派生類提供一些具體的實現(為子類通過通用、可重用的代碼)。抽象基類可以為任何具體行為提供一個實現,而接口則不能。這種實現重用的方式為我們提供了另一種好處:在擴展系統功能時,通過向基類中添加並實現某種功能,所有的派生類立即擁有該功能,而向接口中添加一個成員,則會破壞所有實現該接口的類。
3.使用接口替代繼承
使用抽象基類還是接口,代表了對日后可能發生的變化兩種不同的態度:將一組功能封裝在一個接口中,作為其他類型的實現契約。而基類則可以再日后進行擴展,這些擴展也會自動的成為子類的一部分。
3.1通過擴展方法來模擬繼承
其實,這兩種方式可以混合使用,也就是說:既可以讓類型支持多個接口,也讓其可以重用。我們知道,接口不能包含實現,也不能包含任何具體的數據成員。但是,擴展方法卻是可以應用在接口上。例如:System.Linq.Enumerable類中就包含了30多個聲明於IEnumerable<T>接口之上的擴展方法,所有實現了該接口的類型都會自動獲得這些擴展方法自身的實現(注意:接口本身並不能包含任何實現,只是通過擴展方法的形式模擬地提供一些實現)。例如,下面的這個針對IEnumerable接口擴展方法:
1 public static class Extensions 2 { 3 /// <summary> 4 /// 為IEnumerable<T>類型添加擴展方法 5 /// </summary> 6 /// <typeparam name="T"></typeparam> 7 /// <param name="sequence"></param> 8 /// <param name="action"></param> 9 public static void ForAll<T>(this IEnumerable<T> sequence, Action<T> action) 10 { 11 foreach (T item in sequence) 12 { 13 action(item); 14 } 15 } 16 }
所有實現了IEnumerable接口的類型,會自動獲取這個方法的實現;同樣的,如果一個類實現了IEnumerable接口,那么它也會獲得該接口的所有擴展方法的實現。
3.2 接口編程的靈活性
在.NET 環境下的繼承是單根繼承,根據接口編程要比根據基類編程擁有更大的靈活性,因為一個類型可以實現多個接口。同樣的不相關的類型也可以實現同一個接口,這對在為不相關類編寫公有邏輯時,使用接口可以簡化你的工作。看下面的示例:假設某個程序需要管理員工、客戶、第三方員工,這些類型並不相關(從繼承體系來看),但是他們有一些公有屬性,例如:名稱、地址、電話等等:

1 public class Employee 2 { 3 public string FirstName{get;set;} 4 public string LastName{get;set;} 5 6 public string LastName 7 { 8 get 9 { 10 return string.Format("{0},{1}",LastName,FirstName); 11 } 12 } 13 //略... 14 } 15 16 public class Customer 17 { 18 public string LastName 19 { 20 get 21 { 22 return CustomerName; 23 } 24 } 25 //略... 26 private string CustomerName; 27 } 28 29 public class Vendor 30 { 31 public string Name 32 { 33 get 34 { 35 return vendorName; 36 } 37 } 38 //... 39 private string vendorName; 40 }
上面顯示姓名屬性,其他屬性略過。現在我們可以把公共屬性抽象成一個接口:
public interface IContactInfo { //姓名 string Name{get;} //聯系電話 PhoneNumber primaryContact{get;} //傳真 PhoneNumber Fax{get;} //住址 Address primaryAddress{get;} }
我們將上面所以類都實現IContactInfo接口:
1 //..其他類 略.. 2 public class Employee:IContactInfo 3 { 4 //...略 5 }
現在我們需要編寫一個為這些類型打印自身信息的新方法:
1 public void PrintMailingLabel(IContactInfo ic) 2 { 3 //...略 4 }
我們可以看到所有的,只要是實現了該接口的類型都可以使用該方法,這得益於將我們公有邏輯放在了接口中。
同時,使用接口定義類的API可會提供更好的靈活性,當類型的屬性以類的形式暴露時,也就暴露了該類的所有接口。而若是以暴露某個接口,那么就可以選擇僅為使用者提供那些必要的方法和屬性。而實現該接口的類屬性實現細節,可能會隨着時間改變。
3.3 在結構中使用接口避免拆箱
接口和抽象基類還有一個不同之處在於,抽象基類僅限於引用類型,而接口則沒有這個限制。當我們將struct裝箱時,該裝箱對象實際上支持struct支持的所有接口。當通過接口指針來訪問該struct時,我們不必拆箱即可訪問到內部數據。如下面的例子:
1 public struct URLInfo : IComparable<URLInfo>, IComparable 2 { 3 private string URL; 4 private string description; 5 6 public int CompareTo(URLInfo other) 7 { 8 return URL.CompareTo(other.URL); 9 } 10 11 #region IComparable 成員 12 13 int IComparable.CompareTo(object obj) 14 { 15 if (obj is URLInfo) 16 { 17 URLInfo other = (URLInfo)obj; 18 return CompareTo(other); 19 } 20 else 21 { 22 throw new ArgumentException("比較的對象不是URLInfo類型"); 23 } 24 } 25 26 #endregion 27 }
由於URLInfo實現了IComparable<T>和IComparable接口,所有我們可以輕松的創建一個保護URLInfo對象的排序鏈表。即使在那些依賴老版本的IComparable的代碼也會減少裝箱和拆箱的次數,因為客戶代碼可以再不拆箱的情況下直接調用IComparable.CompareTo()。
小節
接口是一種按契約設計的方式:一個實現了某個接口的類型,必須提供接口中約定的所有方法實現。抽象基類則為一組項目類型提供了一個共用的共同抽象。仔細理解二者之間的差別,使用我們能夠創建更富表現力和提高應對變化的設計。使用類層次來定義相關的類型,用接口暴露功能,並可以讓不同類型實現這些接口。