C# Interface:介面

2020-07-16 10:04:46
C# 的介面可以看成是一個“技能庫”,繼承一個介面可以看成是“插上一個新的技能庫”,它使得你的型別擁有更多的“技能”。

例如,IComparable 使得型別可以比較大小。介面是一種特殊的抽象類,在其中,不能顯式地實現方法;也就是說,介面只擁有一組方法的簽名。

不過,這個看似已經成為常識的事情在 C# 8 中可能會發生改變:介面也許可以擁有預設方法實現。如果這真的變成了現實,那麼,C# 就成了一個允許多重繼承的語言。

當然,不是繼承了介面就萬事大吉的,必須實現介面的方法才可以真正的擴充套件你的類。例如,必須實現 CompareTo,並在其中解釋如何比較大小。

介面負責對現實生活中 Has A 關係的描述。例如,可以比較大小明顯是某個類擁有(has)的性質或能力,而不是說,某個類是“比較大小”,這在中文上都不通。

介面中的所有方法都是 public 的,也不能再使用其他存取修飾符進行修飾。介面也可以擁有自己的屬性、事件和索引器。

顯式介面實現

如果兩個不同的介面中含有一個相同的方法(相同的名稱,引數和返回型別),那麼,同時繼承這兩個介面的型別在實現該方法之後,兩個介面的方法就會具有相同的行為:
class Program
{
    static void Main(string[] args)
    {
        var cg = new Greeting();
        var chinese = (ChineseGreeting)cg;
        //你好
        chinese.Hello();

        var english = (EnglishGreeting)cg;
        //你好
        english.Hello();
        Console.ReadKey();
    }
}
public interface ChineseGreeting
{
    void Hello();
}
public interface EnglishGreeting
{
    void Hello();
}
public class Greeting : ChineseGreeting, EnglishGreeting
{
    //隱式介面實現導致方法只有一次實現機會
    //兩個介面的Hello將會有相同的行為
    public void Hello()
    {
        Console.Write("你好!");
    }
}
為了區別開不同介面相同方法的實現,我們可以使用顯式介面實現:
public class Greeting : ChineseGreeting, EnglishGreeting
{
    //顯式介面實現
    void ChineseGreeting.Hello()
    {
        Console.Write("你好!");
    }
    void EnglishGreeting.Hello()
    {
        Console.Write("Hello!");
    }
}
用的方式是相同的。在使用顯式介面實現之後,必須將物件轉化為介面型別才可以存取它。因為顯式介面實現的方法預設是介面型別私有的(也不能使用存取修飾符),而且是 sealed。

下面的程式碼試圖從介面的繼承型別呼叫顯式介面實現的方法,這會導致編譯錯誤:

//繼承類不存在此方法,顯式介面實現方法歸介面私有
cg.Hello();

顯式介面實現有著諸多缺點,例如必須手動轉換型別為介面基本類型(如果派生類是值型別,那麼還會導致裝箱),沒有 IDE 智慧感知等。

下面是一個 .NET 中的顯式介面實現的例子:

int 1=1;
//由於 ToSingle 是顯式介面實現,所以必須先轉化為介面基本類型
var s = ((IConvertible) i).ToSingle(null);

直接在 i 上呼叫 ToSingle 無法通過編譯。我們看看 int 中的相關程式碼:

float IConvertible.ToSingle(IFormatProvider provider);

ToSingle 簽名包含了介面型別 IConvertible,說明了 ToSingle 方法使用了顯式介面實現。

顯式介面實現與泛型介面

在泛型還沒出現的年代,實現一個介面有時是很頭疼的。例如,常用的 IComparable 介面中的 CompareTo 方法,輸入引數是 object。

因此,任何輸入都合法,這不僅可能帶來裝箱,還會在內部重新轉化為自己想要的型別時發生錯誤。

顯式介面實現可以在一定程度上避免這個問題:
public class People : IComparable
{
    public string name { get; set; }
    public int age { get; set; }
    //型別安全的CompareTo方法
    public int CompareTo(People p)
    {
        return p.age >= age ? 1 : -1;
    }
    //為了實現ICompareable,不得不顯示介面實現
    int IComparable.CompareTo(object obj)
    {
        //obj轉化為People型別可能會出現問題
        return CompareTo((People)obj);
    }
}
上面的型別中,通過比較年齡來比較 People 類的大小。在接受 object 為引數的 CompareTo 方法中,必須強制將 obj 轉化為 People 類,而這是型別不安全的。

我們通過自己寫一個 CompareTo 方法,並令輸入為 People 類解決這個問題。

但是自己的 CompareTo 方法並不算是 IComparable 的實現,我們還要被迫實現一個 IComparable 規定的 CompareTo 方法。

People p = new People();
People q = new People();
q.CompareTo(p);
//如果物件是 People 型別,存取不到顯式介面實現的 CompareTo 方法
//也就不能傳入任意型別
q.CompareTo(i); //不能通過編譯!

//但是可以通過將型別轉化為介面基本類型
//存取到顯式介面實現的 CompareTo 方法,傳入任意型別
//於是再次失去型別安全性
var q2 = (IComparable) q;
q2.CompareTo(i); //可以通過編譯!

我們看到,顯式介面實現還是不能完全保證型別安全性,當泛型介面出現之後,問題 就解決了:
public class People : IComparable<People>
{
    public string name { get; set; }
    public int age { get; set; }
    //型別安全的CompareTo方法
    public int CompareTo(People p)
    {
        return p.age >= age ? 1 : -1;
    }
}
繼承 IComparable<T> 使得我們不需要再寫一個輸入引數為 object 的 CompareTo 方法,它絕對保證型別安全,而且,由於沒有型別轉換,也就不存在裝箱。

抽象類 VS 介面

介面和抽象類有一些區別,介面是一種特殊的抽象類:
  • 介面不能有非抽象的方法。而抽象類可以有非抽象的方法。
  • 介面和抽象類都不能被範例化。
  • 如果繼承介面,必須實現介面所有的抽象方法。而繼承抽象類的類可以只實現一部分父類別的抽象方法。
  • 一個類可以繼承多個介面,但只能繼承自一個別的類。
  • 不能密封抽象類和介面,因為這破壞了介面和抽象類本身的性質,即被人繼承。

抽象類和它的子類之間應該是一般和特殊的關係,而介面僅僅是它的子類應該實現的一組規則。當邏輯為一般和特殊的關係,而特殊本身無法或無需有任何方法的實現時,則考慮使用抽象類。

當邏輯為“能做什麼”時,使用介面。這樣的例子太多了,例如 C# 中所有的資料結構都繼承介面 IEnumerable,意味著所有的資料結構都擁有列舉的能力。

另外,如果你的型別繼承了介面 IDisposable,則擁有 Dispose 的能力。

如果基本類型發生變化(例如新增了方法),那麼派生型別將自動繼承該方法,不需要更改程式碼。

但如果介面新增了方法,那麼所有派生型別都必須更改程式碼才能通過編譯。在這種情形之下,要不就老老實也改程式碼,要不就考慮將新方法做成一個新的介面。

介面不繼承自 Object

介面不繼承自任何型別,它旨在提供一個乾淨的方法庫和固定的簽名,並等待繼承者來實現。

如果介面繼承自 Object,那麼它就會具有 Object 的方法了。但是,任何型別其實都已經繼承了 Object,所以型別再繼舉一個繼承了 Object 方法的介面,就會導致毫無意義的重複。

介面方法分派

假設有一個介面 ITest,含有方法A,—個型別 B 繼承了該介面,並實現了 B.A,那麼,實際上 B.A 是被 IL 的 newslot+virtual 修飾的(顯式介面實現的話,則再加一個 final 關鍵字)。

因此,可以在型別 B 的方法表中找到 B.A,然後進行呼叫。而相對的,雖然介面 ITest 是型別 B 的父類別,而且方法 A 隱式的為 virtual,介面方法 ITest.A 是不會出現在型別 B 的方法表中的。