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

阅读 318

推荐阅读