本文转载自微信公众号「码农读书」,作者码农读书 。转载本文请联系码农读书公众号。
C# 8 中新增了一个非常有趣的特性,叫做 默认接口方法 (又称虚拟扩展方法),这篇文章将会讨论 C# 8 中的默认接口方法以及如何使用。
在 C# 8 之前,接口不能包含方法定义,只能在接口中定义方法签名,还有一个就是接口的成员默认是 public 和 abstract , 在 C# 8 之前,接口不能包含字段,也不能包含private, protected, 或者 internal 的方法成员。如果你在接口中引入了一个新成员,默认情况下你必须更新实现该接口的所有子类。
在 C# 8 中可以在接口定义方法的默认实现,而且还可以定义接口成员为 private,protect,甚至是 static,还有一点挺奇葩的,一个接口的 protect 成员是不能被实现类所访问的,相反,它只能在子接口中被访问,接口的 virtual 成员可以由派生接口 override,但不能被派生类 override,还有一点请注意,接口目前还不能定义 实例成员。
为什么要使用默认接口方法
所谓的 默认接口方法 指的是接口中定义了一个默认实现的方法, 如果实现该接口的类没有实现默认接口方法的话,那么这个 默认接口方法 只能从接口上进行访问,这是一个很有用的特性,因为它可以帮助开发人员在不破坏现有功能的情况下向接口的未来版本添加新方法。
考虑下面的 ILogger 定义。
- public interface ILogger
- {
- public void Log(string message);
- }
下面的两个类扩展了ILogger接口并实现了Log()方法。
- public class FileLogger : ILogger
- {
- public void Log(string message)
- {
- //Some code
- }
- }
- public class DbLogger : ILogger
- {
- public void Log(string message)
- {
- //Some code
- }
- }
现在假设你想在ILogger接口中新增一个方法,该方法接受两个参数:一个 文本 一个 日志级别,下面的代码片段展示了日志级别的枚举类。
- public enum LogLevel
- {
- Info, Debug, Warning, Error
- }
修改后的 ILogger 接口如下:
- public interface ILogger
- {
- public void Log(string message);
- public void Log(string message, LogLevel logLevel);
- }
好了,现在问题来了,因为 ILogger 中新增了一个 Log 方法,你必须要在所有实现该接口的所有子类中实现 Log(string message, LogLevel logLevel) 方法,这就很尴尬了,如果不这样做的话,编译器肯定是不会放行的,在现实情况下,这个接口实现类可能在多个 dll 中,甚至在多个团队中,可想而知,这个工作量是非常大并且非常痛苦的。
默认接口方法案例
这就是 默认接口方法 的应用场景,你可以在接口中定义一个默认方法是实现,如下代码所示:
- public interface ILogger
- {
- public void Log(string message);
- public void Log(string message, LogLevel logLevel)
- {
- Console.WriteLine("Log method of ILogger called.");
- Console.WriteLine("Log Level: "+ logLevel.ToString());
- Console.WriteLine(message);
- }
- }
这个时候,实现 ILogger 接口的子类可以不实现新的 Log(string message, LogLevel logLevel) 方法,因此下面的代码也是跑的通的,编译器不会抛出任何错误。
- public class FileLogger : ILogger
- {
- public void Log(string message)
- {
- //Some code
- }
- }
- public class DbLogger : ILogger
- {
- public void Log(string message)
- {
- //Some code
- }
- }
默认接口方法不能被继承
现在创建一个 FileLogger 类实例,然后直接调用新的带参数的 Log() 方法,如下代码所示:
- FileLogger fileLogger = new FileLogger();
- fileLogger.Log("This is a test message.", LogLevel.Debug);
从上面图可看出 默认接口方法 不能被子类继承,换句话说,子类根本就不知道接口中还有带参数的 Log() 方法。
默认接口方法和菱形问题
现在有一个非常重要的问题,默认接口方法如何避免 菱形问题?换句话说就是 接口的 多继承 问题,考虑下面的代码清单。
- public interface A
- {
- public void Display();
- }
- public interface B : A
- {
- public void Display()
- {
- Console.WriteLine("Interface B.");
- }
- }
- public interface C : A
- {
- public void Display()
- {
- Console.WriteLine("Interface C.");
- }
- }
- public class MyClass : B, C
- {
- }
当编译上面代码时,会抛出一个编译错误,说 MyClass 没有实现 A.Display() 方法,解决这个问题很简单,在 MyClass 中实现一下接口方法就可以了,如下代码所示:
- public interface A
- {
- public void Display();
- }
- public interface B : A
- {
- public void Display()
- {
- Console.WriteLine("Interface B.");
- }
- }
- public interface C : A
- {
- public void Display()
- {
- Console.WriteLine("Interface C.");
- }
- }
- public class MyClass : B, C
- {
- public void Display()
- {
- Console.WriteLine("MyClass.");
- }
- }
接下来就可以生成 MyClass 实例了,然后再调用 Display() 方法,如下代码所示:
- static void Main(string[] args)
- {
- A obj = new MyClass();
- obj.Display();
- Console.Read();
- }
现在问题来了,到底是哪一个 Display() 方法被调用了呢?为了避免歧义,C# 将会使用最近覆盖规则,即 Class.Display() 方法被最先调用。
抽象类 VS 接口
到这里,我想你肯定有疑问,抽象类 和 接口 是不是很相似了,甚至可以互换了?虽然抽象类和接口现在看起来在很多方面都很相似,但两者之间还是有微妙的区别的,具体如下:
- 抽象类可以有实例成员,接口则不能。
- 抽象类不能多继承,接口还是可以的。
默认接口方法 允许开发人员利用 trait 编程技术,该技术可以让那些附属于该方法的不相关类型得以继续使用,可能你有点懵,我举个例子:假设你构建好了一个dll,被很多的开发人员所使用,现在你要发布该 dll 的新版本,比如说往接口中添加了新方法,这个时候你可以定义默认实现,这样就可以对已使用的开发者进行无感升级。
译文链接:https://www.infoworld.com/article/3455239/how-to-use-default-interface-methods-in-csharp-8.html