0. 文章目的
??面向有一定基礎的C#初學者,介紹C#中介面的意義、使用以及特點,
1. 閱讀基礎
??了解C#基本語法(如定義一個類、繼承一個類)
??理解OOP中的基本概念(如繼承,多型)
2. 什么是介面
2.1 現實中的協定與介面
??貓貓頭在整理電腦檔案,需要一個小工具來分類檔案,于是貓貓頭向群里求助:
??“有沒有小伙伴幫我用Objective-C做一個分類檔案的小工具”
??群里沒有人回答,貓貓頭意識到可能是因為會Objective-C的人比較少,于是改問:
??“有沒有小伙伴幫我用Rust做一個分類檔案的小工具”
??群里依然沒有人回答,貓貓頭意識到可能是會Rust的人比較少,但貓貓頭此時還意識到,自己只是需要獲一個可以分類檔案的小工具,用什么語言好像并不重要,于是,貓貓頭想了一下,改問:
??“有沒有小伙伴可以幫我做一個分類檔案的小工具”
??很快,群里有人用Shell幫貓貓頭寫了一個小工具,貓貓頭用小工具很快完成了任務,
??上述例子中,貓貓頭在請求幫助時,給出了一個可以幫忙上的‘前提’,即可以提供一個可以分類檔案的小工具,而通過這個前提,貓貓頭的朋友知道如何幫助貓貓頭,我們將這種用于指示兩個物體之間(比如貓貓頭和TA的朋友之間)如何互動的‘前提’稱之為‘協定’,
??協定的最大意義在于規范了不同物件間的互動方式,一個物件如果想要知道如何與另一個物件互動,只需要了解與該物件互動所需要遵守的協定,而不需要考慮該物件的具體情況,就像貓貓頭只需要一個能分類檔案的小工具,而幫忙的朋友到底如何實作這個小工具其實并無所謂,
??而現實中所謂的介面就是一種協定,例如設備的USB充電介面,任何只要滿足USB規范的連接線都可以接入其充電介面并為其供電,而至于電從哪里來設備本身并不關心,介面是使物件得以模塊化的重要概念,介面定義了一種‘只要滿足即可互動’的規范,這可以極大程度上降低物件之間的耦合,
??對于編程語言來說,介面的主要作用也是用于為各個模塊之間做出協定,通過協定,模塊之間知道如何進行互動,而不需要為各種模塊進行特別編碼,以此可以最大程度減小模塊間的耦合度,
2.2 繼承與抽象類
(1)基石:繼承與多型
??在具體討論介面是什么之前,需要先知道介面最早的樣子是什么,首先我們定義一個Animal類,定義一個名為MakeSound的虛方法:
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("make some noise");
}
}
??接下來從Animal派生出兩個類,并重寫其MakeSound方法:
class Cat
{
public override void MakeSound()
{
Console.WriteLine("meow meow");
}
}
class Dog
{
public override void MakeSound()
{
Console.WriteLine("woof woof");
}
}
??由于Cat與Dog繼承自Animal,因此像下面這樣代碼是可以使用的:
void Pet(Animal animal)
{
animal.MakeSound();
}
Cat cat = new Cat();
Pet(cat);
Dog dog = new Dog();
Pet(dog);
(會輸出meow meow與woof woof)
??Animal類中定義了MakeSound方法,因此其肯定有一個MakeSound方法可以呼叫,而Cat與Dog都是Animal的子類,因此兩者也肯定有一個MakeSound方法可以呼叫,所以上述代碼是安全的,當呼叫時,由于多型,上述代碼會輸出‘meow meow’與‘woof woof’,
(2)進一步抽象
??上面的代碼中,Animal對MakeSound方法的定義的唯一意義在于保證了其有一個名為MakeSound的方法可以呼叫,而上文的Pet方法里呼叫Animal物件的MakeSound方式時,因為多型,實際呼叫的是Cat與Dog中重寫MakeSound的方法,也就是說,Animal如何實作MakeSound方法并不重要,因此完全可以不提供Animal中MakeSound方法的實作:
class Animal
{
public virtual void MakeSound()
{
// 方法實作并不重要
};
}
??然后另一方面,很多時候我們希望呼叫的MakeSound方法能做一些有意義的事,這就需要繼承自Animal的類都重寫該方法,所以我們可能還希望可以在編碼時要求Animal的子類重寫其MakeSound方法,
??C#提供了實作上述需求的方法,在C#中,有一種特殊的類叫做抽象類(abstract class),這種類不允許實體化,并且允許定義一種被稱為抽象方法(abstarct method)的方法,抽象方法指的是在當前類中不提供實作,轉而由子類提供實作的方法,下面是將Animal類轉化為抽象類的示例:
abstract class Animal
{
public abstract void MakeSound(); // 抽象方法不需要也不能提供方法實作,實作由子類完成
}
??(在class關鍵字前添加abstract可將類定義為抽象類,為方法添加abstract修飾符可將方法定義為抽象方法)
??現在,繼承自Animal類的型別都必須重寫其抽象方法MakeSound了,重寫抽象方法和重寫一般的虛方法一致:
class Cat
{
public override void MakeSound()
{
Console.WriteLine("meow meow");
}
}
class Dog
{
public override void MakeSound()
{
Console.WriteLine("woof woof");
}
}
??使用上也沒有什么區別:
void Pet(Animal animal)
{
animal.MakeSound();
}
Cat cat = new Cat();
Pet(cat);
Dog dog = new Dog();
Pet(dog);
??不知道看到這里你是否意識到了什么:Animal類保證了其子類有一個MakeSound方法可以被呼叫,所以Pet方法可以假定所有Animal的子類都有一個MakeSound方法,因此可以安全呼叫,也就是說,Animal類向外保證了其子類必然有一個MakeSound方法可以呼叫,而繼承自Animal的Cat與Dog類都實作了Animal對外的保證,
(3)介面
??再進一步來看,其實Pet方法中只需要物件可以提供MakeSound方法即可,至于物件的型別是否與Animal之間存在‘IS-A’關系并不重要,也就是說,對Pet方法來說只要物件可以‘保證’有一個MakeSound方法可以呼叫即可,現在我們將這一保證抽象出來,并用一個語意更清晰的類名來指代這種保證:
abstract class CanMakeSound
{
public abstract void MakeSound();
}
??再進一步,在語言層面上提供語法支持來將其與抽象類區分開(定義為一個‘保證’,而不是一個類),我們使用interface來表示這一保證:
interface CanMakeSound
{
public abstract void MakeSound();
}
??由于這只是一種保證,所有里面的方法都只是一種協定(表示有某個方法可以呼叫),因此方法只需要方法簽名即可(回傳型別+方法名+引數串列),另外,既然是‘對外保證’可以呼叫某一方法,那么方法也應該是public的,所以可以默認所有的方法宣告都是public的抽象方法:
interface CanMakeSound
{
void MakeSound();
}
??這樣,通過一步步對抽象類的提煉,我們提出了介面(interface)的這一概念,在簡化語法的同時,CanMakeSound提供的‘保證’也有了更明確的語意,
??這就是介面的本質:協定行為,指明‘可以做什么’,即‘CAN-DO’,介面是一個概念,不同的語言對介面概念的實作方式不同,例如C#為介面提供了語言級的支持,而一些沒有為介面提供語言級支持的編程語言也可以通過上述的抽象類來模擬介面,例如C++,另一方面,C#中的介面其實也可以視為一種特殊的抽象類,
??不過在繼續之前,我們來看看如果C#沒有對介面的支持將會是什么樣,在上述例子中我們一開始用抽象類來模擬介面,由于C#不支持多重繼承,因此無法同時實作多個抽象類,如果要表示實作多個‘介面’就不得不一層一層繼承下去,這會使編碼變得復雜以至于難以維護:
abstract class Walkable { ... }
abstract class Flyable { ... }
class WalkableCat : Walkable { }
class WalkableAndFlyableCat : Flyable { }
class SuperCat : WalkableAndFlyableCat { }
??上述代碼中SuperCat使用了兩個輔助類WalkableCat和WalkableAndFlyableCat才得以同時實作Walkable與Flyable,甚至概念上來說SuperCat只是WalkableAndFlyableCat的子類,而不是有‘可以做什么’的保證,語意上也缺乏清晰度,因此,C#將介面視為一種專門的型別,并提供語言級的支持是很有必要的,
3. C#中的介面
3.1 定義介面
??要在C#中定義一個介面,需要使用interface關鍵字,并在介面中定義協定的方法,下面是一個介面定義:
interface IFlyable
{
void Fly();
}
(根據C#的編碼建議,介面命名應該以大寫字母I開頭)
??上述代碼中,介面IFlyable協定了實作該介面的的型別都會有一個Fly方法可以呼叫,你可以把它想象成下面這樣的抽象類:
abstract class IFlyable
{
public abstract void Fly();
}
??對比兩者,可以發現定義介面時除了將abstract class替換為了interface外,對方法也沒有用public與abstract修飾,這一原因在前文已經提及過:由于介面本身就是用于對外協定行為,因此介面協定的方法自然應該是可以被外部訪問的public,同時介面中的方法只是提供一種協定,因此無需提供實作,
??當然,一個介面中可以協定多個方法:
interface IFlyable
{
void Prepare();
void Fly();
}
??但是,介面中不能定義欄位:
interface IFlyable
{
string Name; // 不允許定義欄位
}
??這是由于介面的作用是協定行為,而所謂的行為就是方法,但是,你可以在介面中定義屬性甚至事件:
interface IFlayable
{
event Action Prepared;
string Name { get; set; }
string this[int index] { get; set; } // 索引器也是屬性(有引數性)
}
??這是由于屬性本質上是方法,事件本質上是對多播委托的方法封裝,因此上面介面定義的實際含義如下:
interface IFlayable
{
// event Action Prepared;
void add_PreparedHandler(Action action); // 注冊委托
void remove_PreparedHandler(Action action); // 取消注冊委托
// string Name { get; set; }
string get_Name(); // 獲取Name屬性
void set_Name(string val); // 設定Name屬性
// string this[int index] { get; set; }
string get_Item(int index); // 獲取Item屬性
void set_Name(int index, string val); // 設定Item屬性
}
3.2 使用介面
3.2.1 實作介面
??介面定義后,就可以讓型別去實作了,要讓一個型別實作介面很簡單,只需要‘繼承’該介面,然后實作該介面中協定過的方法即可,例如下面用SuperCat實作IFlyable介面:
interface IFlyable
{
void Fly();
}
class Cat { }
class SuperCat : Cat, IFlayable
{
public void Fly()
{
Console.WriteLine("Flying");
}
}
??上述代碼中特地讓SuperCat繼承自Cat類,只是為了說明實作一個介面的語法和繼承一個型別的語法相似,并且介面的位置要位于基類之后,所以下面這樣是不行的:
class SuperCat : IFlayable, Cat { } // 錯誤,基類要在介面位置之前
??實作介面的方法時只需要保證方法簽名相同(回傳型別+方法名+引數串列),而方法本身可以添加async、unsafe等方法修飾符,因此下面SuperCat中的Fly方法也可以實作IFlyable:
class SuperCat : Cat, IFlayable
{
public unsafe async void Fly()
{
Console.WriteLine("Flying");
}
}
??另外,與繼承類最大的不同在于,型別可以實作多個介面,只實作了各個介面協定的方法即可:
// SuperCat繼承自Cat
class SuperCat : Cat, IFlyable, IWalkable, ISoundMaker
{
// 實作IFlyable
// 實作IWalkable
// 實作ISoundMaker
}
3.2.2 呼叫介面
??可以像使用抽象類參考一樣使用介面參考,如下面使用IFlyable:
void Call(IFlyable flyable)
{
flyable.Fly(); // IFlyable介面協定了實作該介面的方法都有一個Fly方法可以呼叫
}
SuperCat superCat = new SuperCat();
Call(superCat);
??簡而言之,將介面視為一個抽象類使用即可,
3.3 進階介面操作
3.3.1 介面“繼承”
??介面與介面之間也可以繼承,例如:
interface IMachine
{
void Launch();
}
interface IGameConsole : IMachine
{
void PlayGame();
}
??上述代碼中IGameConsole介面“繼承”了IMachine介面,這意味著實作IGameConsole介面時除了要實作其協定的PlayGame方法外,也需要實作IMachine介面中協定的Launch方法:
class Xbox : IGameConsole
{
public void Launch() { ... } // IMachine協定的Launch方法
public void PlayGame() { ... } // IGameConsole協定的PlayGame方法
}
??之所以要為“繼承”添加引號,是因為介面間的繼承行為從概念上來說應該稱為‘組合’,也就是說,IGameConsole并不是繼承了IMachine介面,而是對IMachine介面進行了組合,請記住這個概念,在后文中會提及原因,
3.3.2 顯式實作介面
??有時候多個介面協定的方法之間可能存在沖突,例如:
interface IGameConsole
{
void Launch(); // 啟動游戲
}
interface IMachine
{
void Launch(); // 啟動機器
}
class Xbox : IGameConsole, IMachine
{
public void Launch() { ... } // 只能定義一個Launch
}
??上述代碼中的介面IGameConsole與IMachine都協定了一個Launch方法,然而兩個Launch方法所做的事并不一樣,因此需要分別對其進行實作,但顯然你只能定義一個Launch方法,要解決此類問題,就需要顯式實作介面,以上述情況為例,顯式實作介面的方法如下:
class Xbox : IGameConsole, IMachine
{
void IGameConsole.Launch() { ... } // 實作IGameConsole的Launch
void IMachine.Launch() { ... } // 實作IMachine的Launch
}
??有兩個需要點需要關注:
- 顯式實作的方法沒有訪問修飾符
- 顯式實作的方法名為
介面名.方法名
??由于顯式實作的方法沒有訪問修飾符,意味著其訪問權限是默認的private,外部無法直接呼叫,但可以通過介面參考來呼叫相應的方法:
XBox xbox = new XBox();
// xbox.GetInformation(); // 不能直接呼叫
IGameConsole console = xbox; // 使用IGameConsole參考
ConsoleInfo a = console.GetInformation(); // 呼叫Xbox中的IGameConsole.GetInformation
IMachine machine = xbox; // 使用IMachine參考
MachineInfo b = machine.GetInformation(); // 呼叫Xbox中的IMachine.GetInformation
??另外,對于類內部來說,也需要使用介面參考來呼叫:
class Xbox : IGameConsole, IMachine
{
void IGameConsole.Launch() { ... } // 實作IGameConsole的Launch
void IMachine.Launch() { ... } // 實作IMachine的Launch
void Test()
{
IGameConsole console = this; // 使用介面參考呼叫IGameConsole的Launch方法
console.Launch();
}
}
??順便一提,由于要通過介面參考來呼叫方法,因此對值型別來說此時將會面臨裝箱問題,一般情況下,應該盡可能選擇默認的實作方式(即通過定義方法簽名相同的方法)而非顯式實作,
??現在回過頭來談論一下為什么介面之間的“繼承”實際是‘組合’,還是對于下述列子:
interface IMachine
{
void Launch(); // 這個Launch用來啟動機器
}
interface IGameConsole : IMachine
{
void Launch(); // 這個Launc用來啟動游戲
}
class Xbox : IGameConsole
{
void IGameConsole.Launch() { ... }
void IMachine.Launch() { ... }
}
??你應該很快能理解上述代碼的意圖:IGameConsole組合了IMachine介面,Xbox實作IGameConsole介面,并顯式實作了兩個GetInformation方法,如果介面之間是繼承,那么上述代碼從概念上說不通:IGameConsole繼承自IMachine,然后重寫了其同名的Launch方法,然而我們在實作介面的時候卻分別實作了IMachine和IGameConsole的Launch方法,那么IGameConsole到底繼承了IMachine什么?另一方面來講,協定的行為又如何用繼承關系描述?總不能說‘可以啟動游戲’繼承了‘可以開機’吧,因此,應當認識到介面之間的“繼承”實質是組合,即將協定進行組合,
3.3.3 介面方法的默認實作
??盡管介面協定的方法默認是抽象方法,但是你確實可以在介面中為協定的方法提供實作,這種方法被稱為默認介面方法:
interface IGameConsole
{
void Launch()
{
Console.WriteLine("Launched");
}
}
class Xbox : IGameConsole
{
// 此時不需要為IGameConsole協定的Launch方法提供實作
}
Xbox xbox = new Xbox();
// 由于Launch方法在Xbox中沒有宣告為public,因此需要通過介面參考來呼叫
IGameConsole console = xbox;
console.Launch();
(輸出:Launched)
??實作介面時可以不實作介面中提供了默認實作的方法,但是由于方法沒有在實作該介面的型別中定義為public,因此此時該方法對外部來說無法訪問,此時則同樣需要通過介面參考來呼叫相應方法,另外,不必擔心兩個介面的默認實作出現沖突,如下:
interface IGameConsole
{
void Launch()
{
Console.WriteLine("GameConsole Launched");
}
}
interface IMachine
{
void Launch()
{
Console.WriteLine("Machine Launched");
}
}
class Xbox : IGameConsole, IMachine
{
}
??這是因為如果XBox沒有實作IGameConsole與IMachine的Launch方法,那么外部要使用這兩個介面協定的方法就只能通過介面參考,那么這時候顯然可以明確要呼叫的方法:
Xbox xbox = new Xbox();
IGameConsole console = xbox;
console.Launch(); // IGameConsole默認實作的Launch
IMachine machine = xbox;
machine.Launch(); // IMachine默認實作的Launch
??此外,如果對介面組合時出現方法沖突,編譯器會給出警告,此時可以使用new關鍵字來抑制警告:
interface IMachine
{
void Launch()
{
Console.WriteLine("Machine Launched");
}
}
interface IGameConsole : IMachine
{
new void Launch() // new的含義是:本型別中與基類中相似簽名的成員沒有關系
{
Console.WriteLine("GameConsole Launched");
}
}
(同樣此時只能通過介面參考來呼叫介面方法,因此也不會出現沖突)
??最后,如果型別中實作了介面的默認方法(無論是默認實作還是顯式實作),那么就會覆寫默認實作:
interface IGameConsole
{
void Launch()
{
Console.WriteLine("Launched");
}
}
class Xbox : IGameConsole
{
public void Launch() // 實作IGameConsole的Launch方法
{
Console.WriteLine("Xbox Launched");
}
}
Xbox xbox = new Xbox();
IGameConsole console = xbox;
console.Launch(); // 此時呼叫的是Xbox中定義的Launch
(上述代碼輸出‘Xbox Launched’)??
??然而,不推薦使用介面默認實作,因為介面本身應該只提供協定功能,一般情況下如果需要有默認實作更應該考慮使用基類而不是介面,或者定義一個實作了該介面的類,并在這個類中提供實作:
interface IGameConsole { ... }
class GameConsoleBase : IGameConsole { ... }
??通常真正需要默認實作的場合是需要更新某個介面,但又不希望影響之前已經使用了該介面的代碼,
3.3.4 為介面添加靜態方法
??你可以在介面中定義靜態方法:
interface IFoo
{
static void Hello()
{
Console.WriteLine("Hello");
}
}
??其使用和使用一般類的靜態方法沒有區別:
IFoo.Hello();
??靜態方法不屬于介面的協定,因此實作介面時不需要實作介面中的靜態方法,你可以把介面中的靜態方法理解為一個由該介面管理的方法,不過,C#目前有一項預覽功能,可以像協定普通方法一樣協定靜態方法,這在后文會提到,
3.3.5 指定介面成員的訪問修飾符
??介面成員默認的訪問修飾符為public,但你可以指定為其他修飾符:
interface IFoo
{
private void PrivateMethod() // private訪問限制,必須提供默認實作
{
Console.WriteLine("Require defualt Implement");
}
protected void ProtectedMethod(); // protected訪問限制,可在組合了該介面的介面中使用
internal void InternalMethod(); // internal訪問限制,同一程式集范圍內可用
public void PublicMethod(); // pubilc訪問限制,默認的訪問限制
// ... 還有一些很少用的組合訪問修飾符,這里就不提了
}
??而當訪問限制為private時,由于這個方法只能用在介面內部訪問,無法在介面外為其提供實作,所以必須為其提供默認實作,通常private訪問級別是用于實作默認介面方法的輔助方法,另外再特別說明一下訪問限制為protected時的情況,當介面中協定的方法的訪問限制為protected時,如果要實作該方法,則必須顯式實作,否則不會被視為該方法的實作:
class Foo : IFoo
{
void IFoo.ProtectedMethod() { ... } // 顯式實作IFoo中的ProtectedMethod
protected void ProtectedMethod() { ... } // 只是定義了一個ProtectedMethod方法而已,與介面無關
}
??另外,由于你只是‘實作’了該介面而不是‘繼承’了該介面,所以你也無法呼叫其方法:
class Foo : IFoo
{
void IFoo.ProtectedMethod() { ... }
void Test()
{
IFoo foo = this;
foo.ProtectedMethod(); // Foo和介面IFoo之間不是繼承關系,無法呼叫
}
}
??這類方法只能在組合該介面的介面中呼叫:
interface IFoo
{
protected void ProtectedMethod();
}
interface Foo2 : IFoo
{
void DoSomething()
{
ProtectedMethod(); // 可以訪問IFoo中的ProtectedMethod方法
}
}
??你可能覺得這種既不能被外部訪問也不能被內部訪問的方法沒什么用,不過下面是一個使用例子:
查看代碼
interface ISpeaker
{
protected void Say(); // protected訪問權限,只能在介面內以及組合了該介面的介面內使用
}
interface IRepeater : ISpeaker // IRepeater組合了ISpeaker
{
void Repeat() // Repeat的默認實作,呼叫三次ISpeaker協定的Say方法
{
Say();
Say();
Say();
}
}
class HelloRepeater : IRepeater
{
void ISpeaker.Say() // 顯式實作了ISpeaker中協定訪問等級為protected的Say方法
{
Console.WriteLine("Hello!");
}
}
class WorldRepeater : IRepeater
{
void ISpeaker.Say() // 顯式實作了ISpeaker中協定訪問等級為protected的Say方法
{
Console.WriteLine("World!");
}
}
void CallRepeater(IRepeater speaker)
{
speaker.Repeat();
}
CallRepeater(new HelloRepeater());
CallRepeater(new WorldRepeater());
(輸出三次Hello!與三次World!) ??
??盡管如此,通常來說訪問級別為protected的介面方法確實沒什么明顯的作用,但在一些特殊的情況下可能對于封裝和修改現有系統有幫助,
3.4 特殊介面
3.4.1 泛型介面
??可以宣告一個泛型介面:
interface IDataObject<T>
{
T GetData();
}
??上面定義了一個泛型介面IDataObject<>,泛型介面并不神秘,就如可以像理解抽象類一樣去理解介面,同樣可以用理解泛型類的方式去理解泛型介面,由于泛型不是本文重點故不多做闡述,不過,為了提高泛型介面的實用性,泛型介面還支持將型別引數宣告為協變數或逆變數,關于協變與逆變在另一篇文章里有所闡述,這里也不再贅述,
3.4.2 協定靜態成員的介面
??前文提到過介面允許協定靜態成員,但截至目前這是C#的一項預覽功能,需要在專案的csproj組態檔中向PropertyGroup添加EnablePreviewFeatures以啟用語言的預覽功能:
<EnablePreviewFeatures>True</EnablePreviewFeatures>
??一個協定了靜態方法的介面如下(注意由于介面中本身可以定義靜態方法,所以需要添加abstract關鍵字來指示其為抽象方法):
interface IGameConsole
{
static abstract void PrintInfo();
}
??以及一個實作該介面協定的靜態方法的類:
class Xbox : IGameConsole
{
public static void PrintInfo()
{
Console.WriteLine("A famous game console");
}
}
??咋一看好像協定靜態方法的意義不大,因為呼叫靜態方法需要直接使用型別名,無法通過介面參考來呼叫,但是,考慮以下代碼:
interface IAddable<T> where T : IAddable<T>
{
static abstract T operator +(T left, T right);
}
class Math<T> where T : IAddable<T>
{
public static T Add(T left, T right)
{
return left + right;
}
}
??由于運算子多載的本質是定義一個靜態方法,因此協定靜態方法意味著可以對型別允許使用的運算子進行協定,意味著可以在泛型中假定泛型型別可以進行+-*/等運算,這對于泛型約束來說非常有用,要知道在相當的一段時間里C#是沒有辦法假定泛型型別可以使用運算子的,需要說明的是你可能注意到上述代碼中泛型介面IAddable<>的 型別引數T的泛型約束是where T : IAddable<T>,看起來有點別扭,這是因為運算子多載要求其引數型別至少有一個是當前型別,因此需要T是IAddable<T>型別(即自己),
4. 介面雜談
4.1 介面在繼承鏈中的傳遞
??首先定義一個介面,基類以及其子類:
interface IGameConsole
{
public void Launch();
}
class Xbox : IGameConsole
{
public void Launch()
{
Console.WriteLine("Xbox Launched!");
}
}
class XboxOne : Xbox { }
??既然基類實作了介面,那么其子類必然也滿足介面的實作,因此下面的代碼可以按預期運行:
IGameConsole console = new XboxOne(); // IGameConsole可以參考XboxOne
console.Launch(); // 輸出Xbox Launched!
??子類自然可以重新實作父類中實作過的介面協定,但是要分情況:
(1)父類中的方法不是虛方法,需要重新實作介面:
class XboxOne : Xbox, IGameConsole // 宣告重新實作IGameConsole
{
public new void Launch()
{
Console.WriteLine("Xbox One Launched!");
}
}
??(方法中的new修飾符不是必須的,但是加上會讓語意更清晰)
??這樣在使用介面參考時才能呼叫到正確的方法:
IGameConsole console = new XboxOne();
console.Launch(); // 輸出Xbox One Launched!
??而下面的實作無法正確重新實作:
class XboxOne : Xbox
{
public void Launch()
{
Console.WriteLine("Xbox One Launched!");
}
}
??畢竟Xbox的Launch方法只是一個普通非虛方法,只是它可以實作IGameConsole介面的協定而已,
(2)父類中的方法為虛方法,直接重寫該虛方法即可:
class XboxOne : Xbox
{
public override void Launch() // 如果基類的Launch方法被修飾為虛方法,則直接重寫即可
{
Console.WriteLine("Xbox One Launched!");
}
}
??當然,嚴格來說這是多型的功勞(雖然介面本身的實作也依賴多型就是了),
4.2 多重實作
??可以同時對介面進行默認實作和顯式實作:
interface IGameConsole
{
void Launch();
}
class Xbox : IGameConsole
{
public void Launch() // 默認實作
{
Console.WriteLine("Xbox Launched!");
}
void IGameConsole.Launch() // 顯式實作
{
Console.WriteLine("A Game Console Launched");
}
}
??不過從實際上來說,同時定義默認實作和顯式實作后,真正實作介面的是顯式實作,而默認實作此時就只是普通的方法而已,原因是因為使用介面無外乎通過下面兩方式:
Xbox xbox = new Xbox();
xbox.Launch(); // 輸出Xbox Launched!
IGameConsole console = xbox;
console.Launch(); // 輸出A Game Console Launched
??第一是直接通過物件使用,但其實這和介面無關,這本身就只是一個普通的方法呼叫,
??第二是通過介面參考呼叫,這時候介面參考將呼叫顯式實作,因此顯式實作才是真正的實作,
??這是合理的,因為顯式實作的語意更明確,
5. 使用建議
5.1 基類 or 介面?
??使用基類有時也可以做到介面能做的事,而另一方面在面向介面編程成為一種流行時可能讓人在更應該使用基類的地方使用介面,關于選擇基類還是介面,主要有如下幾點可以考慮:
(1)IS-A還是CAN-DO,在OOP中繼承主要是描述型別間的IS-A關系,例如Cat繼承自Animal,因為Cat是一種(IS-A)Animal,而介面主要用于表示‘CAN-DO’,即表示型別‘可以做什么’,
(2)是否需要儲存狀態或定義復雜行為,基類可以定義欄位,提供更豐富的方法實作,而介面不能定義欄位并且方法通常都是抽象方法(不推薦使用默認實作),
(3)行為是否比型別重要,例如某一位置只需要使用型別FileUtil的某個方法來獲取文本資料,那么此時重要的是獲取文本資料,至于資料是不是FileUtil提供的其實并不重要,此時若將獲取資料這一行為抽象為介面可以獲得提供更好的泛用性,
5.2 對介面的一些使用建議
(1)介面名以I開頭,并且盡可能以‘CAN-DO’風格命名,或者合適時使用名詞亦可,下面是良好的介面名例子:
interface IEnumerable { ... } // 表示可遍歷
interface IList { ... } // 表示可以進行類似串列的操作
interface IFlyable { ... } // 表示能飛
interface IDataProvider { ... } // 表示能提供資料
(2)介面一旦定義后應該盡可能避免修改,因為介面是協定,型別之間基于介面的協定而做出假設進行互動,隨意更改會使程式的維護變得復雜,
(3)介面的協定應該盡可能少,能滿足其介面名描述的行為即可,多個簡單的介面組合勝過一個所謂的“萬能”介面,
(4)介面應該提供詳細的注釋,以闡述該介面的協定,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/488371.html
標籤:C#
