C#8.0之后接口已經不再單純了,我懵逼了!


一:背景

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

圖片名稱


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM