写点什么

C# 8 中的默认接口方法

  • 2018-06-24
  • 本文字数:5998 字

    阅读完需:约 20 分钟

关键要点

  • 默认接口方法已经被包含在 C# 8 的新功能建议中,开发人员可以像使用 trait 那样使用默认方法。
  • trait 是面向对象的编程技术,用于提升不相关类之间方法的重用性。
  • C#语言开发人员基于 Java 的默认方法概念开发此功能。
  • C#通过在运行时调用最具体的覆盖方法来解决默认接口方法可能会发生的钻石继承问题。
  • 在使用默认接口方法时,C#编译器将尽量让开发者免于发生许多常见的实现错误。

默认接口方法(也称为虚拟扩展方法)是 C#8 的一项新功能建议,开发人员可以像使用 trait 那样使用默认方法。trait 是面向对象的编程技术,用于提升不相关类之间方法的重用性。

在这篇文章中,我将介绍这个新功能,包括新的 C#语法,以及这个功能如何让你的代码更加干净和紧凑。

默认方法带来的主要好处是,现在可以在不破坏实现类的情况下给接口添加默认方法。换句话说,这个特性让开发者可以选择是否要覆盖默认方法。

下面描述的日志记录示例是该功能的一个非常好的使用场景。ILogger 接口有一个抽象的 WriteLogCore 方法。其他方法都是默认方法,如 WriteError 和 WriteInformation,它们通过不同的参数调用 WriteLogCore。ILogger 实现类只需要实现 WriteLogCore 方法即可。

可以想象一下,你的继承类为此可以省去多少代码。不过,这个功能虽好,但也存在风险,因为它是一种多重继承。它也存在钻石继承问题,下面将作具体描述。另外,接口方法必须是没有状态的“纯行为”,这意味着接口仍然像过去一样不能直接引用其他字段。

接口语法已经经过扩展,可接受下面列出的新关键字。例如,你可以在接口中编写一个私有方法,代码仍然可以通过编译并正常工作。

  • 方法体或索引器、属性、事件访问器
  • private、protected、internal、public、virtual、abstract、override、sealed、static、extern
  • 静态字段
  • 静态方法、属性、索引器和事件
  • 具有默认访问权限的显式访问修饰符是 public 的
  • Override 修饰符

不允许出现:

  • 实例状态、实例字段、实例自动属性

默认接口方法示例

下面这个简单的例子演示了如何使用这一特性。

复制代码
// ------------------------Default Interface Methods---------------------------
interface IDefaultInterfaceMethod
{
public void DefaultMethod()
{
Console.WriteLine("I am a default method in the interface!");
}
}
class AnyClass : IDefaultInterfaceMethod
{
}
class Program
{
static void Main()
{
IDefaultInterfaceMethod anyClass = new AnyClass();
anyClass.DefaultMethod();
}
}

控制台输出:

> I am a default method in the interface!可以看到,接口提供了默认方法,实现类并不知道接口提供了默认方法,也不包含该接口方法的实现。

将 IDefaultInterfaceMethod 更改为 AnyClass,如下所示:

复制代码
AnyClass anyClass = new AnyClass();
anyClass.DefaultMethod();

上面的代码会产生编译时错误:AnyClass 不包含 DefaultMethod。

这证明了实现类对默认方法一无所知。

图1:在类上调用默认方法时的错误消息

要访问默认接口方法,必须将其转型成接口:

复制代码
AnyClass anyClass = new AnyClass();
((IDefaultInterfaceMethod)anyClass).DefaultMethod();

控制台输出:

> I am a default method in the interface!值得一提的是,相同的功能在 Java 中已经存在了很长时间,.NET 团队已经将 Java 默认方法文档作为.NET Framework 开发人员的参考,例如:

“我们应该更深入地了解 Java 在这方面所做的工作,他们肯定已经积累了很多这方面的见解。” —— C#语言设计笔记 2017 年 4 月 11 日

接口中的修饰符

正如我之前提到的,接口语法现在可以接受以下关键字:protected、internal、public 和 virtual。默认情况下,默认接口方法是 virtual 的,除非使用了 sealed 或 private 修饰符。类似的,没有方法体的接口成员默认是 abstract 的。

例如:

复制代码
// ------------------------ Virtual and Abstract---------------------------
interface IDefaultInterfaceMethod
{
// By default, this method will be virtual, and the virtual keyword can be here used!
virtual void DefaultMethod()
{
Console.WriteLine("I am a default method in the interface!");
}
// By default, this method will be abstract, and the abstract keyword can be here used
abstract void Sum();
}
interface IOverrideDefaultInterfaceMethod : IDefaultInterfaceMethod
{
void IDefaultInterfaceMethod.DefaultMethod()
{
Console.WriteLine("I am an overridden default method!");
}
}
class AnyClass : IDefaultInterfaceMethod, IOverrideDefaultInterfaceMethod
{
public void Sum()
{
}
}
class Program
{
static void Main()
{
IDefaultInterfaceMethod anyClass = new AnyClass();
anyClass.DefaultMethod();
IOverrideDefaultInterfaceMethod anyClassOverridden = new AnyClass();
anyClassOverridden.DefaultMethod();
}
}

控制台输出:

复制代码
> I am a default method in the interface!
> I am an overridden default method!

关键字 virtual 和 abstract 可以从接口中删除,不过删不删除其实对编译后的代码并没有任何影响。

注意:在覆盖的方法中不允许出现访问修饰符。

覆盖示例:

复制代码
interface IOverrideDefaultInterfaceMethod : IDefaultInterfaceMethod
{
public void IDefaultInterfaceMethod.DefaultMethod()
{
Console.WriteLine("I am an overridden default method");
}
}

上面的代码会产生编译时错误:修饰符“public”在此处无效。

图2:修改器在重写的方法中是不允许的

钻石继承问题

这个问题指的是因为允许多重继承而产生的模糊性。对于允许多重继承的语言(如C++)来说,这是一个很大的问题。然而,在C#中,类不允许多重继承,接口也只在有限的范围内进行多重继承,而且不包含状态。

图3:钻石依赖关系

考虑以下情况:

复制代码
// ------------------------Diamond inheritance and classes---------------------------
interface A
{
void m();
}
interface B : A
{
void A.m() { System.Console.WriteLine("interface B"); }
}
interface C : A
{
void A.m() { System.Console.WriteLine("interface C"); }
}
class D : B, C
{
static void Main()
{
C c = new D();
c.m();
}
}

上面的代码会产生编译时错误,如图 4 所示:

图 4:钻石问题的错误消息

.NET 开发团队决定通过在运行时调用最具体的覆盖方法来解决钻石问题。

“实现了接口成员的类应该总是胜过接口提供的默认实现,即使它是从基类继承的。只有当类没有提供具体的实现时,才考虑使用默认实现“

如果你想了解更多关于此问题的信息,可以参看提案:默认接口方法 C#语言设计笔记 2017 年 4 月 19 日

回到我们的例子。问题是编译器无法推断出最具体的覆盖方法是哪个。不过,你可以像下面这样在类 D 中添加方法“m”,现在编译器就可以使用这个类实现来解决钻石问题。

复制代码
class D : B, C
{
// Now the compiler will use the most specific override, which is defined in the class ‘D’
void A.m()
{
System.Console.WriteLine("I am in class D");
}
static void Main()
{
A a = new D();
a.m();
}
}

控制台输出:

> I am in class Dthis 关键字

下面的例子演示了如何在接口中使用“this”关键字。

复制代码
public interface IDefaultInterfaceWithThis
{
internal int this[int x]
{
get
{
System.Console.WriteLine(x);
return x;
}
set
{
System.Console.WriteLine("SetX");
}
}
void CallDefaultThis(int x)
{
this[0] = x;
}
}
class DefaultMethodWithThis : IDefaultInterfaceWithThis
{
}

客户端代码:

复制代码
IDefaultInterfaceWithThis defaultMethodWithThis = new DefaultMethodWithThis();
Console.WriteLine(defaultMethodWithThis[0]);
defaultMethodWithThis.CallDefaultThis(0);

控制台输出:

复制代码
0
SetX

ILogger 示例

ILogger 接口是解释默认方法技术的最常用示例。在我的代码示例中,包含了一个名为“WriteCore”的抽象方法,其他方法都有一个默认的实现。ConsoleLogger 和 TraceLogger 实现了 ILogger 接口。下面的这些代码非常紧凑和干净。在过去,一个类除非是抽象类,否则必须实现接口所有的方法,这可能导致很多重复代码。而使用新的方法,ConsoleLogger 将能够继承另一个类层次结构,换句话说,默认方法将为你提供最灵活的设计。

复制代码
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!

Player 示例

这是一款包含不同类型玩家的游戏。力量型玩家具有更大的攻击力,而限制型玩家具有更小的攻击力。

复制代码
public interface IPlayer
{
int Attack(int amount);
}
public interface IPowerPlayer: IPlayer
{
int IPlayer.Attack(int amount)
{
return amount + 50;
}
}
public interface ILimitedPlayer: IPlayer
{
int IPlayer.Attack(int amount)
{
return amount + 10;
}
}
public class WeakPlayer : ILimitedPlayer
{
}
public class StrongPlayer : IPowerPlayer
{
}

客户端代码:

复制代码
IPlayer powerPlayer = new StrongPlayer();
Console.WriteLine(powerPlayer.Attack(5)); // Output 55
IPlayer limitedPlayer = new WakePlayer();
Console.WriteLine(limitedPlayer.Attack(5)); // Output 15

正如你在上面的代码示例中看到的那样,IPowerPlayer 接口和 ILimitedPlayer 接口包含了默认实现。限制型玩家攻击力更小。如果我们定义一个新的类,例如 SuperDuperPlayer(继承自 StrongPlayer),那么新类会自动从接口中获得默认的强攻击力行为,如下所示。

复制代码
public class SuperDuperPlayer: StrongPlayer
{
}
IPlayer superDuperPlayer = new SuperDuperPlayer();
Console.WriteLine(superDuperPlayer.Attack(5)); // Output 55

Generic Filter 示例

ApplyFilter 是一个默认接口方法,它包含了一个应用在泛型类型上的 Predicate。在我的例子中,使用了一个虚拟的过滤器来模拟行为。

复制代码
interface IGenericFilter<T>
{
IEnumerable<T> ApplyFilter(IEnumerable<T> collection, Func<T, bool> predicate)
{
foreach (var item in collection)
{
if (predicate(item))
{
yield return item;
}
}
}
}
interface IDummyFilter<T> : IGenericFilter<T>
{
IEnumerable<T> IGenericFilter<T>.ApplyFilter(IEnumerable<T> collection, Func<T, bool> predicate)
{
return default;
}
}
public class GenericFilterExample: IGenericFilter<int>, IDummyFilter<int>
{
}

客户端代码:

复制代码
IGenericFilter<int> genericFilter = new GenericFilterExample();
var result = genericFilter.ApplyFilter(new Collection<int>() { 1, 2, 3 }, x => x > 1);

控制台输出:

复制代码
2, 3

客户端代码:

复制代码
IDummyFilter<int> dummyFilter = new GenericFilterExample();
var emptyResult = dummyFilter.ApplyFilter(new Collection<int>() { 1, 2, 3 }, x => x > 1);

控制台输出:

0你可以将此通用过滤器概念应用在其他设计上。

限制

在接口中使用修饰符关键字时,首先需要了解一些限制和注意事项。在很多情况下,编译器会为我们检测常见错误(例如下面列出的错误)。

例如下面的代码:

复制代码
interface IAbstractInterface
{
abstract void M1() { }
abstract private void M2() { }
abstract static void M3() { }
static extern void M4() { }
}
class TestMe : IAbstractInterface
{
void IAbstractInterface.M1() { }
void IAbstractInterface.M2() { }
void IAbstractInterface.M3() { }
void IAbstractInterface.M4() { }
}

上面的代码将产生下面列出的编译时错误:

复制代码
error CS0500: 'IAbstractInterface.M1()' cannot declare a body because it is marked abstract
error CS0621: 'IAbstractInterface.M2()': virtual or abstract members cannot be private
error CS0112: A static member 'IAbstractInterface.M3()' cannot be marked as override, virtual, or abstract
error CS0179: 'IAbstractInterface.M4()' cannot be extern and declare a body
error CS0122: 'IAbstractInterface.M2()' is inaccessible due to its protection level

错误 CS0500 表示默认方法“IAbstractInterface.M3()”不能是抽象的,因为它有方法体。错误 CS0621 表示该方法不能是既是 private 又是 abstract 的。

在 Visual Studio 中:

图 5:Visual Studio 中的编译错误

更多信息和源代码:

关于作者

Bassam Alugili 是 STRATEC AG 的高级软件专家和数据库专家。STRATEC 是全球领先的全自动分析器系统软件合作商,专注于实验室数据管理和智能消耗品的软件系统。

查看英文原文 Default Interface Methods in C# 8

2018-06-24 12:373146
用户头像

发布了 731 篇内容, 共 433.4 次阅读, 收获喜欢 1997 次。

关注

评论

发布
暂无评论
发现更多内容

看山聊Java:开始使用 Java8 中的时间类

看山

Java java8 DATE类型 10月月更

回忆录:「技术主管」练成记

蔡建斌

管理 引航计划 内容合集

5. 再次接触装饰器,增加一种数据结构代替 if_else 的写法

梦想橡皮擦

10月月更

linux之man命令

入门小站

Linux

【Vuex 源码学习】第七篇 - Vuex 的模块安装

Brave

源码 vuex 10月月更

架构实战营 - 模块九作业

Julian Chu

架构实战营

Kotlin中逻辑运算符操作分析

maijun

and kotlin逻辑运算符 &&

007云原生之Service Mesh(中心化Broker)

穿过生命散发芬芳

云原生 10月月更

白月光与朱砂痣-Flannel略糙,Cilium太美

Lance

在线base64加密解密工具

入门小站

工具

golang--进程,线程,协程调度

en

Go 语言

项目开发过程中,成员提离职,怎么办?

石云升

项目管理 管理 引航计划 内容合集 10月月更

移动端网络监控实践

轻口味

android 大前端 网络协议 引航计划 10月月更

云原生训练营20210915-作业1

笑春风

Ember Data 之模型定义

devpoint

model ember.js 10月月更

在线摇骰子/色子工具

入门小站

工具

SpringMVC源码分析-HandlerAdapter(7)-ServletInvocableHandlerMethod组件分析

Brave

源码 springmvc 10月月更

果然爆发!!!央行数字货币要在这“特殊”的一天正式推出?

CECBC

Ember Data 之记录查询

devpoint

store ember.js 10月月更

【初恋系列】那年的试卷我们再肝一遍(试卷存储详细设计)

人工智能~~~

存储 详细设计 那年的试卷我们再肝一遍 试题

【LeetCode】窥探迭代器Java题解

Albert

算法 LeetCode 10月月更

架构实战营 模块九(毕业设计) 作业

一雄

作业 架构实战营 毕业设计 模块九

Redis 面试那些事(30问与答)

Seven七哥

redis 面试 后端

【Flutter 专题】38 图解 Android 打包 APK 文件

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 10月月更

5分钟搞懂URI、URL和URN

俞凡

网络 10月月更

15个开发者最常犯的错误,你中招了吗?

俞凡

认知 10月月更

一种基于Kotlin DSL的静态代码分析AST规则扩展实现

maijun

Java dsl 静态代码分析 结构化规则 规则扩展

区块链技术可以在哪些方面颠覆石油和天然气行业?

CECBC

🍃【SpringBoot技术专题】「开发实战系列」动态化Quartz任务调度机制+实时推送任务数据到前端

洛神灬殇

springboot quartz DeferredResult 任务调度 10月月更

容器 & 服务:Helm Charts(三)K8s集群信息

程序员架构进阶

架构 Kubernetes 容器 Helm Charts 10月月更

代码要写注释了吗?

HelloWorld杰少

领航计划

C# 8中的默认接口方法_.NET_Bassam Alugili_InfoQ精选文章