.NET C#基礎(7):介面 - 人如何和貓互動

HiroMuraki發表於2022-06-10

0. 文章目的

  面向有一定基礎的C#初學者,介紹C#中介面的意義、使用以及特點。

 

1. 閱讀基礎

  瞭解C#基本語法(如定義一個類、繼承一個類)

  理解OOP中的基本概念(如繼承,多型)

 

2. 什麼是介面

2.1 現實中的協定與介面

  貓貓頭在整理電腦檔案,需要一個小工具來分類檔案,於是貓貓頭向群裡求助:

  “有沒有小夥伴幫我用Objective-C做一個分類檔案的小工具”

  群裡沒有人回答,貓貓頭意識到可能是因為會Objective-C的人比較少,於是改問:

  “有沒有小夥伴幫我用Rust做一個分類檔案的小工具”

  群裡依然沒有人回答,貓貓頭意識到可能是會Rust的人比較少,但貓貓頭此時還意識到,自己只是需要獲一個可以分類檔案的小工具,用什麼語言好像並不重要。於是,貓貓頭想了一下,改問:

  “有沒有小夥伴可以幫我做一個分類檔案的小工具”

  很快,群裡有人用Shell幫貓貓頭寫了一個小工具,貓貓頭用小工具很快完成了任務。

 

  上述例子中,貓貓頭在請求幫助時,給出了一個可以幫忙上的‘前提’,即可以提供一個可以分類檔案的小工具,而通過這個前提,貓貓頭的朋友知道如何幫助貓貓頭。我們將這種用於指示兩個實體之間(比如貓貓頭和他的朋友之間)如何互動的‘前提’稱之為‘協定’。

  協定的最大意義在於規範了不同物件間的互動方式,一個物件如果想要知道如何與另一個物件互動,只需要瞭解與該物件互動所需要遵守的協定,而不需要考慮該物件的具體情況。就像貓貓頭只需要一個能分類檔案的小工具,而幫忙的朋友到底如何實現這個小工具其實並無所謂。

  而現實中所謂的介面就是一種協定,例如裝置的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
}

  有兩個需要點需要關注:

  1. 顯式實現的方法沒有訪問修飾符
  2. 顯式實現的方法名為介面名.方法名

  由於顯式實現的方法沒有訪問修飾符,意味著其訪問許可權是預設的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)介面應該提供詳細的註釋,以闡述該介面的協定。

相關文章