翻譯自 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 ↩︎