C# 8: 預設介面方法

技術譯民發表於2020-10-20

翻譯自 John Demetriou 2018年8月4日 的文章 《C# 8: Default Interface Methods》[1],補充了一些內容

C# 8 之前

今天我們來聊一聊預設介面方法。聽起來真的很奇怪,不是嗎?介面僅用於定義契約。介面的實現類會擁有一組公共方法,不過實現類被賦予了以其自己的方式實現每個方法的自由。目前為止,如果我們還需要為這些方法中的一個或多個方法提供實現,我們將使用繼承。
如果我們希望這個類不是實現所有方法,而只是實現其中的一個子集,我們可以將這些方法和類本身抽象(abstract)。

例如,我們不能這麼寫:

interface IExample
{
    void Ex1();                                      // 允許
    void Ex2() => Console.WriteLine("IExample.Ex2"); // 不允許(C# 8 以前)
}

我們不得不用下面的抽象類來替代:

abstract class ExampleBase
{
    public abstract void Ex1();
    public void Ex2() => Console.WriteLine("ExampleBase.Ex2");
}

不過還好,這已經足夠滿足我們的大部分需求了。

C# 8 之後

那麼,有什麼改變嗎?為什麼我們需要引入這個新特性?我們錯過了什麼並且從未注意到我們錯過了什麼?

菱形問題

由於菱形問題[2],C#(以及許多其他語言)不支援多重繼承。為了允許多重繼承,同時避免菱形問題,C# 8 引入了預設介面方法。

從 C# 8 開始,使用預設介面方法,您可以擁有一個介面定義,以及該定義中某些或所有方法的預設實現。

interface IExample
{
    void Ex1();                                      // 允許
    void Ex2() => Console.WriteLine("IExample.Ex2"); // 允許
}

因此,現在您可以實現一個含有已實現方法的介面,並且可以避免希望從特定類(也包含通用方法)繼承的類中的程式碼重複。

使用預設介面方法,菱形問題並沒得到百分之百解決。當一個類繼承自從第三個介面繼承而來的兩個介面,並且所有介面都實現了相同方法時,仍然可能發生這種情況。
在這種情況下,C# 編譯器將根據當前上下文選擇呼叫適當的方法。如果無法推斷出特定的哪一個,則會顯示編譯錯誤。

例如,假設我們有以下介面:

interface IA
{
    void DoSomething();
}

interface IB : IA
{
    void DoSomething() => Console.WriteLine("I am Interface B");
}

interface IC : IA
{
    void DoSomething() => Console.WriteLine("I am Interface C");
}

然後,我建立一個實現上述兩個介面的類 D,會引發一個編譯錯誤:

//編譯器提示:“D”未實現介面成員“IA.DoSomething()”
public class D : IB, IC
{ }

但是,如果類 D 實現它自己版本的 DoSomething 方法,那麼編譯器將知道呼叫哪個方法:

public class D : IB, IC
{
    public void DoSomething() => Console.WriteLine("I am Class D");
}

若 Main 方法程式碼如下:

static void Main()
{
    var x = new D();
    x.DoSomething();
    Console.ReadKey();
}

執行程式,控制檯視窗輸出:I am Class D

其他益處

使用方法的預設介面實現,API 提供者可以擴充套件現有介面而不破壞遺留程式碼的任何部分。

Trait 模式

譯者注:
在計算機程式設計中,特徵(Trait)是物件導向程式設計中使用的一個概念,它表示可用於擴充套件類的功能的一組方法。[3]

Trait 模式大體上就是多個類需要的一組方法。
在此之前,C# 中的 Trait 模式是使用抽象類實現的。但是由於多重繼承不可用,實現 Trait 模式變得非常棘手,所以大多數人要麼避開它,要麼迷失在一個巨大的繼承鏈中。

不過,在介面中使用預設方法實現,這將發生改變。我們可以通過在介面中使用預設介面方法實現,提供一組需要類擁有的方法,然後讓這些類繼承此介面。
當然,任何一個類都可以用它們自己的實現覆蓋這些方法,但是以防它們不希望這麼做,我們為它們提供了一組預設的實現。

以下為譯者補充

介面中的具體方法

預設介面方法的最簡單形式是在介面中宣告具體方法,該方法是具有主體部分的方法。

interface IA
{
    void M() { Console.WriteLine("IA.M"); }
}

實現此介面的類不必實現其具體方法。

class C : IA { } // OK

static void Main()
{
    IA i = new C();
    i.M(); // 輸出 "IA.M"
}

CIA.M 的最終替代是在 IA 中宣告的具體方法 M
請注意,類只能實現介面,而不會從介面繼承成員

C c = new C(); // 或者 var c = new C();
c.M();         // 錯誤: 類 'C' 不包含 'M' 的定義

但如果實現此介面的類也實現了具體方法,則同一般的介面含義是一樣的:

class C : IA
{
    public void M() { Console.WriteLine("C.M"); }
}

static void Main()
{
    IA i = new C();
    i.M(); // 輸出 "C.M"
}

子介面如何呼叫父介面的方法?

這是 willamyao 提的一個挺有意思的問題。乍一看,現實中還會遇到這樣的需求?細想一下,還真的可能會用到。下面就來演示一個簡單的示例,在介面 IB 中呼叫父介面 IA 中的成員方法 M,程式碼如下:

interface IA
{
    void M() { Console.WriteLine("IA.M"); }
}

interface IB : IA
{
    //void IA.M() { Console.WriteLine("IB.M"); }

    void IB_M() { M(); }
}

class C : IB { }

static void Main(string[] args)
{
    IB i = new C();
    i.IB_M();  // 輸出 "IA.M";如果把 IB 中的註釋行開啟,這裡會輸出 "IB.M"
}

作者 : John Demetriou
譯者 : 技術譯民
出品 : 技術譯站
連結 : 英文原文


  1. https://www.devsanon.com/c/c-8-default-interface-methods/ C# 8: Default Interface Methods ↩︎

  2. https://www.cnblogs.com/ittranslator/p/13838080.html 菱形問題 ↩︎

  3. https://en.wikipedia.org/wiki/Trait_(computer_programming) Trait ↩︎

相關文章