写点什么

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:373157
用户头像

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

关注

评论

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

北美亚特兰大一金融服务公司面试总结

HoneyMoose

鹅厂疯子整理了万字Java笔记!小白:硬核资源基础知识已入门

牛哄哄的java大师

Java Object

fastadmin+xunsearch题库系统搭建教程

一颗小树

php thinkphp fastadmin xunsearch 题库系统

Excel用户如何学习数据分析语言DAX?

博文视点Broadview

Serverless的定义

刘宇

​太厉害了,终于有人把Spring条件注解讲明白了,送你上岸!

飞飞JAva

spring

IT 专业大学生被培训机构“渗透”情况调查

梦想橡皮擦

签约计划

本文标题不能描述本文内容

小天同学

读书 哲学 读后感 4月日更

了解代理服务器

进击的梦清

nginx Linux 运维 代理原理

A “word-wrap” functionality(一个字符串包裹函数)

HoneyMoose

InfoQ & 声网Agora 技术开放日邀请函

Jessie

音视频 声网

引入:从云计算到Serverless

刘宇

万字长文讲述我是怎样保送清华的|寒门学子的奋斗史(四)

程序猿石头

程序员 码农 逆袭 大学总结 读书总结

网络攻防学习笔记Day1

穿过生命散发芬芳

5月日更 网络攻防

对于即将工作的IT大学生,该如何变强?

cv君

程序人生 IT 科技 问卷 有意义

技术探索系列 - 轻松带你掌握 JMM(1)

洛神灬殇

Java JVM JMM 并发 5月日更

将本地文件/文章上传到 GitHub 的流程

彭宏豪95

git GitHub 效率 编程

【LeetCode】员工的重要性Java题解

Albert

算法 LeetCode 5月日更

音频变速变调原理及soundtouch代码分析

floer rivor

音视频

聆听极致 ——声网 Agora

cv君

算法 音视频 科技 声网 引航计划

你的烂代码终于有了解决方案

博文视点Broadview

【得物技术】网络优化——域名解析原理&实践

得物技术

网络 域名解析 域名 得物技术 实践

已跪!Java全能笔记爆火,Java教程/Java包/Eclipse安装指南全有

牛哄哄的java大师

Java

又一个免费良心的下载站,答应我:别再下到流氓软件了。

彭宏豪95

ios 效率 工具 下载 4月日更

当代软件IT大学生的技术学习之路

Nydia

签约计划

2021年十大突破性技术

石云升

读书笔记 5月日更

高校软件IT专业大学生课外培训调查问卷

穿过生命散发芬芳

行业分析能力考核

面试:某云面试题目整理

程序员架构进阶

Java 面试 自我提升 28天写作 4月日更

SpringCloud-技术专题-Feign组件基本使用(2)

洛神灬殇

springmvc SpringCloud Hystrix Fegin

手机屏幕投屏到桌面的离线方案

黄敏

BPF 之巅:洞悉 Linux 系统和应用性能

博文视点Broadview

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