接口
什么是接口
接口是指定一組函數成員而不實現它們的引用類型。所以只能類和結構來實現接口。
這種描述比較抽象,直接來看個示例。
下例中,Main方法創建並初始化了一個CA類的對象,並將該對象傳遞給PrintInfo方法。
class CA { public string Name; public int Age; } class CB { public string First; public string Last; public double PersonsAge; } class Program { static void PrintInfo(CA item) { Console.WriteLine("Name: {0},Age {1}",item.Name,item.Age); } static void Main() { CA a=new CA(){Name="John Doe",Age=35}; PrintInfo(a); } }
只要傳入的是CA類型的對象,PrintInfo就能正常工作。但如果傳入的是CB,就不行了。
現在的代碼不能滿足上面的需求,原因有很多。
- PrintInfo的形參指明了實參必須為CA類型的對象
- CB的結構與CA不同,字段的名稱和類型與CA不一樣
接口解決了這一問題。
- 聲明一個IInfo接口,包含兩個方法–GetName和GetAge,每個方法都返回string
- 類CA和CB各自實現了IInfo接口,並實現了兩個方法
- Main創建了CA和CB的實例,並傳入PrintInfo
- 由於類實例實現了接口,PrintInfo可以調用那兩個方法,每個類實例執行各自的方法,就好像是執行自己類聲明中的方法
interface IInfo //聲明接口 { string GetName(); string GetAge(); } class CA:IInfo //聲明了實現接口的CA類 { public string Name; public int Age; public string GetName(){return Name;} public string GetAge(){return Age.ToString();} } class CB:IInfo //聲明了實現接口的CB類 { public string First; public string Last; public double PersonsAge; public string GetName(){return First+""+Last;} public string GetAge(){return PersonsAge.ToString();} } class Program { static void PrintInfo(IInfo item) { Console.WriteLine("Name: {0},Age {1}",item.GetName(),item.GetAge()); } static void Main() { var a=new CA(){Name="John Doe",Age=35}; var b=new CB(){First="Jane",Last="Doe",PersonsAge=33}; PrintInfo(a); PrintInfo(b); } }
使用IComparable接口的示例
- 第一行代碼創建了包含5個無序整數的數組
- 第二行代碼使用了Array類的靜態Sort方法來排序元素
- 用foreach循環輸出它們,顯式以升序排序的數字
var myInt=new[]{20,4,16,9,2}; Array.Sort(myInt); foreach(var i in myInt) { Console.WriteLine("{0}",i); }
Sort方法在int數組上工作良好,但如果我們嘗試在自己的類上使用會發生什么呢?
class MyClass { public int TheValue; } ... MyClass[] mc=new MyClass[5]; ... Array.Sort(mc);
運行上面的代碼,將會得到一個異常。Sort並不能對MyClass對象數組排序,因為它不知道如何比較自定義的對象。Array類的Sort方法其實依賴於一個叫做IComparable的接口,它聲明在BCL中,包含唯一的方法CompareTo。
public interface IComparable { int CompareTo(object obj); }
盡管接口聲明中沒有為CompareTo方法提供實現,但IComparable接口的.NET文檔中描述了該方法應該做的事情,可以在創建實現該接口的類或結構時參考。
文檔中寫到,在調用CompareTo方法時,它應該返回以下幾個值:
- 負數值 當前對象小於參數對象
- 整數值 當前對象大於參數對象
- 零 兩個對象相等
我們可以通過讓類實現IComparable來使Sort方法可以用於MyClass對象。要實現一個接口,類或結構必須做兩件事情:
- 必須在基類列表后面列出接口名稱
- 必須實現接口的每個成員
例:MyClass中實現了IComparable接口
class MyClass:IComparable { public int TheValue; public int CompareTo(object obj) { var mc=(MyClass)obj; if(this.TheValue<mc.TheValue)return -1; if(this.TheValue>mc.TheValue)return 1; return 0; } }
例:完整示例代碼
class MyClass:IComparable { public int TheValue; public int CompareTo(object obj) { var mc=(MyClass)obj; if(this.TheValue<mc.TheValue)return -1; if(this.TheValue>mc.TheValue)return 1; return 0; } } class Program { static void PrintInfo(string s,MyClass[] mc) { Console.WriteLine(s); foreach(var m in mc) { Console.WriteLine("{0}",m.TheValue); } Console.WriteLine(""); } static void Main() { var myInt=new[] {20,4,16,9,2}; MyClass[] mcArr=new MyClass[5]; for(int i=0;i<5;i++) { mcArr[i]=new MyClass(); mcArr[i].TheValue=myInt[i]; } PrintOut("Initial Order: ",mcArr); Array.Sort(mcArr); PrintOut("Sorted Order: ",mcArr); } }
聲明接口
上一節使用的是BCL中已有的接口。現在我們來看看如何聲明接口。
關於聲明接口,需要知道的重要事項如下:
- 接口聲明不能包含以下成員
- 數據成員
- 靜態成員
- 接口聲明只能包含如下類型的非靜態成員函數的聲明
- 方法
- 屬性
- 事件
- 索引器
- 這些函數成員的聲明不能包含任何實現代碼,只能用分號
- 按照慣例,接口名稱以大寫字母I(Interface)開始
- 與類和結構一樣,接口聲明也可以分布
例:兩個方法成員接口的聲明
關鍵字 接口名稱 ↓ ↓ interface IMyInterface1 { int DoStuff(int nVar1,long lVar2); //分號替代了主體 double DoOtherStuff(string s,long x); }
接口的訪問性和接口成員的訪問性之間有一些重要區別
- 接口聲明可以有任何的訪問修飾符public、protected、internal或private
- 然而,接口的成員是隱式public的,不允許有任何訪問修飾符,包括public
接口允許訪問修飾符 ↓ public interface IMyInterface2 { private int Method1(int nVar1); //錯誤 ↑ 接口成員不允許訪問修飾符 }
實現接口
只有類和結構才能實現接口。
- 在基類列表中包括接口名稱
- 實現每個接口成員
例:MyClass實現IMyInterface1接口
class MyClass:IMyInterface1 { int DoStuff(int nVar1,long lVar2) {...} //實現代碼 double DoOtherStuff(string s,long x) {...} //實現代碼 }
關於實現接口,需要了解以下重要事項:
- 如果類實現了接口,它必須實現接口的所有成員
- 如果類從基類繼承並實現了接口,基類列表中的基類名稱必須放在所有接口之前。
基類必須放在最前面 接口名 ↓ ↓ class Derived:MyBaseClass,IIfc1,IEnumerable,IComparable
簡單接口示例
interface IIfc1 { void PrintOut(string s); } class MyClass:IIfc1 { public void PrintOut(string s) { Console.WriteLine("Calling through: {0}",s); } } class Program { static void Main() { var mc=new MyClass(); mc.PrintOut("object"); } }
接口是引用類型
接口不僅是類或結構要實現的成員列表。它是一個引用類型。
我們不能直接通過類對象的成員訪問接口。然而,我們可以通過把類對象引用強制轉換為接口類型來獲取指向接口的引用。一旦有了接口引用,我們就可以使用點號來調用接口方法。
例:從類對象引用獲取接口引用
IIfc1 ifc=(IIfc1)mc; //轉換為接口,獲取接口引用 ifc.PrintOut("interface"); //使用接口的引用調用方法
例:類和接口的引用
interface IIfc1 { void PrintOut(string s); } class MyClass:IIfc1 { public void PrintOut(string s) { Console.WriteLine("Calling through: {0}",s); } } class Program { static void Main() { var mc=new MyClass(); mc.PrintOut("object"); //調用類對象的實現方法 IIfc1 ifc=(IIfc1)mc; ifc.PrintOut("interface"); //調用引用方法 } }
接口和as運算符
上一節,我們已經知道可以使用強制轉換運算符來獲取對象接口引用,另一個更好的方式是使用as運算符。
如果我們嘗試將類對象引用強制轉換為類未實現的接口的引用,強制轉換操作會拋出異常。我們可以通過as運算符來避免該問題。
- 如果類實現了接口,表達式返回指向接口的引用
- 如果類沒有實現接口,表達式返回null
ILiveBirth b=a as ILiveBirth; if(b!=null) { Console.WriteLine("Baby is called: {0}",b.BabyCalled()); }
實現多個接口
- 類或結構可以實現多個接口
- 所有實現的接口必須列在基類列表中並以逗號分隔(如果有基類名稱,則在其后)
interface IDataRetrieve{int GetData();} interface IDataStore{void SetData(int x);} class MyData:IDataRetrieve,IDataStore { int Mem1; public int GetData(){return Mem1;} public void SetData(int x){Mem1=x;} } class Program { static void Main() { var data=new MyData(); data.SetData(5); Console.WriteLine("Value = {0}",data.GetData()); } }
實現具有重復成員的接口
由於接口可以多實現,有可能多個接口有相同的簽名和返回類型。編譯器如何處理這種情況呢?
例:IIfc1和IIfc2具有相同簽名
interface IIfc1 { void PrintOut(string s); } interface IIfc2 { void PrintOut(string t); }
答案是:如果一個類實現了多接口,並且其中有些接口有相同簽名和返回類型,那么類可以實現單個成員來滿足所有包含重復成員的接口。
例:MyClass 類實現了IIfc1和IIfc2.PrintOut滿足了兩個接口的需求。
class MyClass:IIfc1,IIfc2 { public void PrintOut(string s)//兩個接口單一實現 { Console.WriteLine("Calling through: {0}",s); } } class Program { static void Main() { var mc=new MyClass(); mc.PrintOut("object"); } }
多個接口的引用
如果類實現了多接口,我們可以獲取每個接口的獨立引用。
例:下面類實現了兩個具有當PrintOut方法的接口,Main中以3種方式調用了PrintOut。
- 通過類對象
- 通過指向IIfc1接口的引用
- 通過指向IIfc2接口的引用
interface IIfc1 { void PrintOut(string s); } interface IIfc2 { void PrintOut(string t); } class MyClass:IIfc1,IIfc2 { public void PrintOut(string s) { Console.WriteLine("Calling through: {0}",s); } } class Program { static void Main() { var mc=new MyClass(); IIfc1 ifc1=(IIfc1)mc; IIfc2 ifc2=(IIfc2)mc; mc.PrintOut("object"); ifc1.PrintOut("interface 1"); ifc2.PrintOut("interface 2"); } }
派生成員作為實現
實現接口的類可以從它的基類繼承實現的代碼。
例:演示 類從基類代碼繼承了實現
- IIfc1是一個具有PrintOut方法成員的接口
- MyBaseClass包含一個PrintOut方法,它和IIfc1匹配
- Derived類有空的聲明主體,但它派生自MyBaseClass,並在基類列表中包含了IIfc1
- 即使Derived的聲明主體為空,基類中的代碼還是能滿足實現接口方法的需求
interface IIfc1 { void PrintOut(string s); } class MyBaseClass { public void PrintOut(string s) { Console.WriteLine("Calling through: {0}",s); } } class Derived:MyBaseClass,IIfc1 { } class Program { static void Main() { var d=new Derived(); d.PrintOut("object"); } }
顯式接口成員實現
上面,我們已經看到了單個類可以實現多個接口需要的所有成員。
但是,如果我們希望為每個接口分離實現該怎么做呢?這種情況下,我們可以創建顯式接口成員實現。
- 與所有接口實現相似,位於實現了接口的類或結構中
- 它使用限定接口名稱來聲明,由接口名稱和成員名稱以及它們中間的點分隔符號構成
class MyClass:IIfc1,IIfc2 { void IIfc1.PrintOut(string s) {...} void IIfc2.PrintOut(string s) {...} }
例:MyClass為兩個解耦的成員聲明了顯式接口成員實現。
interface IIfc1 { void PrintOut(string s); } interface IIfc2 { void PrintOut(string t); } class MyClass:IIfc1,IIfc2 { void IIfc1.PrintOut(string s) { Console.WriteLine("IIfc1: {0}",s); } void IIfc2.PrintOut(string s) { Console.WriteLine("IIfc2: {0}",s); } } class Program { static void Main() { var mc=new MyClass(); IIfc1 ifc1=(IIfc1)mc; ifc1.PrintOut("interface 1"); IIfc2 ifc2=(IIfc2)mc; ifc2.PrintOut("interface 2"); } }
如果有顯式接口成員實現,類級別的實現是允許的,但不是必需的。顯式實現滿足了類或結構必須實現的方法。因此,我們可以有如下3種實現場景。
- 類級別實現
- 顯式接口成員實現
- 類級別和顯式接口成員實現
訪問顯式接口成員實現
顯式接口成員實現只可以通過指向接口的引用來訪問。即其他的類成員都不可以直接訪問它們。
例:如下MyClass顯式實現了IIfc1接口。注意,即使是MyClass的另一成員Method1,也不可以直接訪問顯式實現。
- Method1的前兩行編譯錯誤,因為方法在嘗試直接訪問實現
- 只有Method1的最后一行代碼才可以編譯,因此它強制轉換當前對象的引用(this)為接口類型的引用,並使用這個指向接口的引用來調用顯式接口實現
class MyClass:IIfc1 { void IIfc1.PrintOut(string s) { Console.WriteLine("IIfc1"); } public void Method1() { PrintOut("..."); //編譯錯誤 this.PrintOut("..."); //編譯錯誤 ((IIfc1)this).PrintOut("..."); ↑ 轉換為接口引用 } }
這個限制對繼承產生了重要影響。由於其他類成員不能直接訪問顯式接口成員實現,衍生類的成員也不能直接訪問它們。它們必須總是通過接口的引用來訪問。
接口可以繼承接口
之前我們已經知道接口實現可以從基類繼承,而接口本身也可以從一個或多個接口繼承。
- 要指定某個接口繼承其他的接口,應在接口聲明中把某接口以逗號分隔的列表形式放在接口名稱的冒號之后
- 與類不同,它在基類列表中只能有一個類名,接口可以在基接口列表中有任意多個接口
- 列表中的接口本身可以繼承其他接口
- 結果接口包含它聲明的所有接口和所有基接口的成員
interface IDataIO:IDataRetrieve,IDataStore { ... }
例:IDataIO接口繼承了兩個接口
interface IDataRetrieve { int GetData(); } interface IDataStore { void SetData(int x); } interface IDaTaIO:IDataRetrieve,IDataStore { } class MyData:IDataIO { int nPrivateData; public int GetData() { return nPrivateData; } public void SetData(int x) { nPrivateData=x; } } class Program { static void Main() { var data=new MyData(); data.SetData(5); Console.WriteLine("{0}",data.GetData()); } }
不同類實現一個接口的示例
interface ILiveBirth //聲明接口 { string BabyCalled(); } class Animal{} //基類Animal class Cat:Animal,ILiveBirth //聲明Cat類 { string ILiveBirth.BabyCalled() { return "kitten"; } } class Dog:Animal,ILiveBirth //聲明DOg類 { string ILiveBirth.BabyCalled() { return "puppy"; } } class Bird:Animal //聲明Bird類 { } class Program { static void Main() { Animal[] animalArray=new Animal[3]; animalArray[0]=new Cat(); animalArray[1]=new Bird(); animalArray[2]=new Dog(); foreach(Animal a in animalArray) { ILiveBirth b= a as ILiveBirth;//如果實現ILiveBirth if(b!=null) { Console.WriteLine("Baby is called: {0}",b.BabyCalled()); } } } }