翻譯自 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"
}
類 C
中 IA.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
譯者 : 技術譯民
出品 : 技術譯站
連結 : 英文原文
https://www.devsanon.com/c/c-8-default-interface-methods/ C# 8: Default Interface Methods ↩︎
https://www.cnblogs.com/ittranslator/p/13838080.html 菱形問題 ↩︎
https://en.wikipedia.org/wiki/Trait_(computer_programming) Trait ↩︎