系列介紹
【五分鍾的dotnet】是一個利用您的碎片化時間來學習和豐富.net知識的博文系列。它所包含了.net體系中可能會涉及到的方方面面,比如C#的小細節,AspnetCore,微服務中的.net知識等等。
5min+不是超過5分鍾的意思,"+"是知識的增加。so,它是讓您花費5分鍾以下的時間來提升您的知識儲備量。
正文
伴隨着 .NET Core 3.0 一起發布的 C# 8 ,從發布至今已經過了快大半年了。如果您細心的話,就能發現在C# 8新增的功能中有一條:“默認接口方法” 。 半年前當我看到這一新特性的時候,我驚呆了,但是驚訝之余是更多的疑惑。因為對於接口這個東西來說,從C#發布至今的十多年里幾乎一直保持它的樣子,然而在C# 8之后,它有了巨大的變化。隨着而來,也是各種爭論的聲音。
很早之前我就想寫這篇文章了,但是由於各種原因一直拖延到了現在。
先讓我們來回顧一下 C# 中原有的接口有什么特點:
- 接口類似於只有抽象成員的抽象基類。 實現接口的任何類或結構都必須實現其所有成員。
- 接口無法直接進行實例化。 其成員由實現接口的任何類或結構來實現。
- 接口可以包含事件、索引器、方法和屬性。
- 接口不含方法的實現。
- 一個類或結構可以實現多個接口。 一個類可以繼承一個基類,還可實現一個或多個接口。
也正是基於這些特點,當我們在接口中為一個方法加上"pulic"等關鍵字的時候,編譯器會提示我們這是一個錯誤的寫法:
interface IRepository
{
//Compile-time error CS0106 The modifier 'public' is not valid for this item.
public void Add();
}
所以更不用談給方法寫一個實現了。這就讓它和 C# 中的另外一種事物行成了鮮明的對比,是的,抽象類。不知道大家有沒有在各種面試中遇到過這樣的提問:“接口能有任何的訪問修飾符嗎?”,“接口和抽象類的區別是什么?”
曾經您可以和自然的脫口而出答案:“沒有修飾符。一個可以有默認方法,一個只能申明方法…………”。 但是從現在開始:這些答案是錯的了。😂
這是微軟MSDN中的設計規范截圖:
上面的圖是我半年前截的圖,今天本來想去找對應的鏈接分享出來,但是發現找不到了。可能…………
新的接口
好了,說了那么多,我們來看看C# 8 為我們改變后的接口是什么樣子:
enum LogLevel
{
Information,
Warning,
Error
}
interface ILogger
{
void WriteCore(LogLevel level, string message);
void WriteInformation(string message)
{
WriteCore(LogLevel.Information, message);
}
void WriteWarning(string message)
{
WriteCore(LogLevel.Warning, message);
}
void WriteError(string message)
{
WriteCore(LogLevel.Error, message);
}
}
class ConsoleLogger : ILogger
{
public void WriteCore(LogLevel level, string message)
{
Console.WriteLine($"{level}: {message}");
}
}
class TraceLogger : ILogger
{
public void WriteCore(LogLevel level, string message)
{
switch (level)
{
case LogLevel.Information:
Trace.TraceInformation(message);
break;
case LogLevel.Warning:
Trace.TraceWarning(message);
break;
case LogLevel.Error:
Trace.TraceError(message);
break;
}
}
}
ILogger consoleLogger = new ConsoleLogger();
consoleLogger.WriteWarning("Cool no code duplication!"); // Output: Warning: Cool no Code duplication!
ILogger traceLogger = new TraceLogger();
consoleLogger.WriteInformation("Cool no code duplication!"); // Cool no Code duplication!
這是我在網上摘取的一部分代碼。是的,您沒有看錯,接口可以實現方法了。並且還可以給它添加上訪問修飾符:
interface IDemoInterface
{
public static int staticIntValue = 123; //Right
public void PulicMethod(){ } //Right
}
就像您所見的一樣,它還可以在內部聲明靜態的數據。
但是下面的寫法依舊會提示錯誤哦:
interface IDemoInterface
{
abstract void M1() { } //Error. 因為有abstract
abstract private void M2() { } //Error
abstract static void M3() { } //Error
static extern void M4() { } //Error.因為有extern
}
爭議點
走到這里,也許您會說:“這不挺好的嗎?好像對我也沒有啥影響。” 確實,假如您不更改接口的簽名,無論您是否在接口中增加默認實現還是某些靜態數據都不會對已有的應用程序造成任何錯誤。
但是如果您經常使用抽象類的話,您就會發現,這樣的接口是不是和抽象類太像了?甚至有點完全掩蓋了抽象類的優勢。
當我半年前看到這一新特性時,我就產生了這樣的疑惑。 這個 “默認方法實現” 的新特性,真的需要嗎?如果需要,那我如何選擇它和抽象類?
結果我發現,大家都對這一特性產生了困惑:
於時,我抱着懷疑的態度在網上到處搜索答案。最后在C# 官方團隊的筆記中我看到了這樣一句話:
這句話的意思大致是:我們應該更深入地研究Java在這里所做的事情,Java對接口的實現很好,我們應該…………(有關該說明的github鏈接可以點擊這里)。
我當時心就涼了半截。不過緩了緩,我鎮定的思考了一下:好的語言設計被借鑒和參考也是很有必要的。 比如現在其它語言都在借鑒C#的await和async。(PS:C#和Typescript怎么越來越像😝)。
那么我們真的需要在接口中提供默認實現嗎?那什么情況下我需要這樣做? 畢竟咱們使用了 C# 這么多年,就算接口沒有提供默認實現也能設計出很好的系統來。所以為了解決上面的疑問,還是得回到接口和抽象類的本質。
按照咱們以往使用接口和抽象類的情況來看:接口表示的是一種行為,"who can"(比如鳥會飛),而基類表示的是一種類別,"is a"(比如麻雀是鳥)。 因此在OOP的世界中,如果咱們細心的來建模的話,我們會把表示行為的共性抽象為一個接口:比如鳥會飛,咱們可以抽象一個IFly的接口。對老版本的 C# 來說,不能提供方法的實現,所以只會有一個Fly() 的方法簽名。而現在我們通過新的特性,我們可以給“飛”這個動作提供一個默認的實現,比如 90%的鳥都是“煽動翅膀起飛”,則我們可以將這個大部分 的操作作為默認實現,而對那些10%的 “小眾” 進行重寫。 也正是由於接口更關注的是“行為”,所以接口中不能存在“狀態”,因此您會發現雖然可以聲明字段了,但是只能聲明靜態字段。而實例化的狀態信息依舊只能通過抽象類來實現。
當然,在現在接口和抽象類建模比較模糊的今天,從技術的實現上來說,其實接口的默認實現並沒有帶來很多技術編碼上的好處。但是如果您堅持好的規范抽象,比如接口開頭就是用“I”,將對象的行為進行抽象提升為接口,也許某一刻您會感受到該特性帶來的改變。
最后,小聲說一句:一鍵三連……。 哦,不對,點個推薦吧.....