写点什么

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

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

关注

评论

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

什么是hpaPaaS平台?低代码和hpaPaaS是什么关系?

优秀

低代码

3DCAT投屏功能升级,助力企业营销与培训

3DCAT实时渲染

虚拟仿真 实时云渲染 3DCAT 企业营销 实时渲染云

「阿里云 RocketMQ 系列公开课」重磅来袭!

阿里巴巴云原生

Apache 阿里云 RocketMQ 云原生 直播

设计稳定的微服务系统时不得不考虑的场景

阿里巴巴云原生

阿里云 微服务 云原生 OpenSergo

mac 本地hive2 安装

飞哥

大数据 hadoop hive

WebRTC 音频抗弱网技术(下)

融云 RongCloud

【刷题记录】8. 字符串转换整数 (atoi)

WangNing

7月月更

mac 本地安装spark

飞哥

大数据 spark 数据仓库

深度揭秘阿里云函数计算异步任务能力

阿里巴巴云原生

阿里云 Serverless 云原生 异步 函数计算

几种跨平台方案的对比

Geek_99967b

小程序 跨平台

Istio整体架构解析

阿泽🧸

istio 7月月更

聊聊 API 管理-开源版 Yapi 到 SaaS 版 Apifox

Liam

瑞云与宜宾职院开展校企合作,同深圳VR联合会共建元宇宙产业学院

3DCAT实时渲染

职业教育 虚拟现实 虚拟仿真 元宇宙

编译器优化那些事儿(4):归纳变量

openEuler

openEuler 毕昇 JDK 开源操作系统 编译器 加速器 编程语言

hive on spark 和 spark on hive

飞哥

大数据 spark hive

小程序遇上Flutter 3.0

Geek_99967b

flutter 小程序

大数据15周作业

Asha

6. 对象存储

MASA技术团队

C# .net 对象存储 框架 Framework

长安链研究笔记-证书生成工具

长安链

一位 sealer maintainer 的心路历程

阿里巴巴云原生

阿里云 开源 云原生 sealer

使用 doscify 将文章写成文档一般丝滑

宁在春

GitHub Pages docsify 7月月更

机器学习-西瓜书小记

AIWeker

机器学习 7月月更

普通二本院校如何从所谓的从寒冬破冰?

KEY.L

7月月更

阿里云架构师马继雨:云超算解决方案全面助力生命科学行业普惠增效

阿里云弹性计算

高性能计算 生命科学 EHPC

一款代替Typora的软件---MarkText

IT蜗壳-Tango

7月月更

基于ResNet50实现宫颈细胞病变分类

逝缘~

分类神经网络 7月月更

避坑:@Around与@Transactional混用导致事务不回滚

Paul

Spring Boot 2 事务失效 spring aop Java’

微服务与Kubernetes容器云的边界

穿过生命散发芬芳

微服务 7月月更

动态广播发送流程

北洋

android 7月月更

React Table 表格组件使用教程 排序、分页、搜索过滤筛选功能实战开发

蒋川

排序 React 表格 组件库

Flutter 实现小姐姐渐现动画效果

岛上码农

flutter ios 前端 安卓开发 7月月更

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