一:背景
1. 講故事
大家在經過物件導向洗禮的時候,都瞭解過介面,而且知道它是一種自上而下的設計思路,舉個例子,我們電腦上都有 USB 2.0 介面,藍芽耳機實現了它可以進行充電,行動硬碟實現了它可以在電腦端顯示硬碟內容,藍芽滑鼠實現了它可以進行滑鼠操控,可以看出USB插口做出來後,誰來實現誰也搞不清楚,實現者能做出什麼東西,誰也不知道,這就是介面的魅力,落實在 C# 上就是介面中那一個一個的 stub 方法,留給未來的有緣人去實現,如下程式碼:
public interface IUsb
{
void Execute();
}
2. 你可能會有的疑惑
有些朋友可能會說,碼農胡言亂語,介面不光可以定義例項方法,還可以定義 屬性,索引器,事件 等等。。。 如下程式碼:
public interface IUsb
{
event Action<string> action;
string Name { get; set; }
string this[string key]
{
get; set;
}
void Execute();
}
哈哈,果然是一個好問題,沒錯,屬性,索引器和事件都可以定義在介面中,但請不要忘了,你列舉的這些都是編譯器層面的語法糖而已,言外之意就是你看過 編譯後的 IL 程式碼嗎? 如下圖所示:
可以看到,那些所謂的語法糖在IL層面統統是方法,這就很好的解釋了為啥介面中只能定義方法的原因。
3. 現在的介面真的變了
然而這種平衡在 C# 8.0 中被打破,現如今的介面除了常規的例項方法,還可以定義任何標記為 static 的欄位,屬性,方法,建構函式 甚至還可以是 例項方法的預設實現,這就很奇葩了。。。不得不大吼一聲,??, 參考程式碼如下:
public interface IUsb
{
//常量
public const string constVal = "";
//靜態欄位
public static int age = 20;
//靜態建構函式
static IUsb() { }
//預設方法實現
void Disco() { Console.WriteLine("Disco..."); }
void Execute();
}
這下把我搞蒙了,目前除了一些例項欄位還不能定義外,其他的都沒有問題了,我相信不久的將來 interface 也會把這個遺憾解決掉,/(ㄒoㄒ)/~~ , 這叫我如何向後來的晚輩解釋呀~~~ 搞的我現在有很多疑惑!
二:筆者的疑惑
1. 介面的預設方法意義何在?
一個事物的出現,必然有它的應用場景,有些朋友可能會談到這樣的場景,當很多類實現了 IUSB 介面之後,如下程式碼:
public interface IUsb
{
void Execute();
}
public class Mp4 : IUsb
{
public void Execute() { }
}
public class Mouse: IUsb
{
public void Execute(){ }
}
由於某些原因我準備在 IUSB 中新增 Disco 方法,這個時候 MP4 和 Mouse 類肯定會報錯,大家都知道這是因為沒有實現 Disco 的方法,如下圖所示:
這個時候該怎麼辦呢? C# 8.0 的介面預設方法就起到作用了,可以直接在原有介面中定義預設方法,對眾多的介面實現者們是無感知的,可以編譯成功,如下圖所示:
一起都很順利,接下來我就迫不及待的呼叫 Disco 方法,程式碼如下:
我去,從圖中看居然說 Mp4 類沒有 Disco 方法,這就很莫名其妙了,氣人,這叫啥預設方法,為了驗證 MP4 類到底有沒有 Disco 方法,一個到位的驗證方式就是用 windbg 看看 MP4 的方法表。
0:000> !do 0x0000021e63c2ab10
Name: DataStruct.Mp4
MethodTable: 00007ff7cd972248
EEClass: 00007ff7cd96c5e8
Size: 24(0x18) bytes
File: E:\net5\ConsoleApp2\ConsoleApp1\bin\Debug\netcoreapp3.1\ConsoleApp1.dll
Fields:
None
0:000> !dumpmt -md 00007ff7cd972248
EEClass: 00007FF7CD96C5E8
Module: 00007FF7CD94F7D0
Name: DataStruct.Mp4
mdToken: 0000000002000004
File: E:\net5\ConsoleApp2\ConsoleApp1\bin\Debug\netcoreapp3.1\ConsoleApp1.dll
BaseSize: 0x18
ComponentSize: 0x0
Slots in VTable: 6
Number of IFaces in IFaceMap: 1
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
00007FF7CD8A0090 00007FF7CD870A78 NONE System.Object.Finalize()
00007FF7CD8A0098 00007FF7CD870A88 NONE System.Object.ToString()
00007FF7CD8A00A0 00007FF7CD870A98 NONE System.Object.Equals(System.Object)
00007FF7CD8A00B8 00007FF7CD870AD8 NONE System.Object.GetHashCode()
00007FF7CD8B0670 00007FF7CD972228 NONE DataStruct.Mp4.Execute()
00007FF7CD8B1030 00007FF7CD972238 JIT DataStruct.Mp4..ctor()
從上面最後6行程式碼可看出,MP4類的方法表中根本就沒有 Disco 方法,說明 MP4 的世界裡根本就沒有這玩意。。。那怎麼樣才能呼叫的上呢?你需要將 mp4 轉成 IUSB 介面,然後再呼叫 Disco
方法就可以了,如下圖所示:
可是即使能執行,又有什麼用呢?反正子類是感知不到這個介面的預設方法,也顛覆了對介面的認知!我是沒有看出有什麼好處,水平有限沒辦法哈。。。
2. 這個場景自有它的解決方案 [擴充套件方法]
剛才有些朋友提到的場景說後續增加介面方法的時候不影響已實現子類修改程式碼,其實不需要這個特性 C# 也能實現,畢竟這麼龐大的類庫程式碼,肯定會有這樣的場景哈,我就拿 List 集合說事,如下程式碼是 List 的類定義:
public class List<T> :IList<T>, ICollection<T>
{
}
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
int IndexOf(T item);
void Insert(int index, T item);
void RemoveAt(int index);
}
public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
void Add(T item);
void Clear();
bool Contains(T item);
void CopyTo(T[] array, int arrayIndex);
bool Remove(T item);
}
可以看到 List 實現了 IList 和 ICollection 共 7 個方法,但大家在用 List 編碼的時候發現其實遠不止這 7 個方法,其他方法的接入(Select,Where)就是通過 C# 特有的 擴充套件方法 機制實現的,對不對,我覺得擴充套件方法就可以很好的解決 預設介面方法
的問題,所以 USB 介面可以用 擴充套件方法 來實現,如下程式碼所示:
static void Main(string[] args)
{
var mp4 = new Mp4();
mp4.Disco();
Console.ReadLine();
}
public static class UsbExtension
{
public static void Disco(this IUsb usb)
{
Console.WriteLine("Disco...");
}
}
三: 總結
總的來說,這是一個顛覆我三觀的特性,破壞了我對介面的認知,不想再說什麼了,大家有什麼妙解,歡迎留言~~~
更多高質量乾貨:參見我的 GitHub: dotnetfly