写点什么

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

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

关注

评论

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

VNC服务安装配置与使用

耳东@Erdong

vnc 10月月更

最长回文子串 -- 三种解答

秦怀杂货店

数据结构 算法 最长回文子串

Golang语言HTTP客户端实践

FunTester

golang 性能测试 HTTP 接口测试 FunTester

linux之iftop命令

入门小站

Linux

看山聊 Java:检查日期字符串是否合法

看山

Java 10月月更

重置不良的编程陋习

devpoint

编码习惯 10月月更

新一代容器平台ACK Anywhere,来了

阿里巴巴中间件

阿里云 云原生 中间件 ACK ACK Anywhere

【LeetCode】无重复字符的最长子串Java题解

Albert

算法 LeetCode 10月月更

Pandas教程:数据类型操作

Peter

Python pandas

接棒运动赛事!工厂里也有热血竞技?

白洞计划

以太坊的黄金时代:模块化的区块链范式的开端

CECBC

设计微博系统中"微博评论"的高性能高可用计算架构

Rabbit

[ Golang 中的 DDD 实践] 值对象

baiyutang

golang 设计模式 领域驱动设计 DDD 10月月更

私有云部署系列之动态获取IP(程序执行)

稻草鸟人

Python

MyBatis 批量插入数据的 3 种方法!

王磊

Java mybatis springboot

【Vuex 源码学习】第九篇 - Vuex 响应式数据和缓存的实现

Brave

源码 vuex 10月月更

【Flutter 专题】34 图解自定义 View 之 Canvas (二)

阿策小和尚

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

Pandas教程:数据处理基石-数据探索

Peter

Python pandas

架构实战课程 模块5作业

Frank

私有分布式账本技术还是公共区块链?

CECBC

BPM软件是什么?BPM软件跟BPA有关联吗?

低代码小观

企业管理 业务流程管理 信息管理

新一代容器平台ACK Anywhere,来了

阿里巴巴云原生

阿里云 云原生 ACK Anywhere

使用 Ansible 部署 Elasticsearch 集群

Se7en

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

王晓宇

架构实战营

“区块链+农业”推动产业“提档升级”

CECBC

在线2-36任意进制转换工具

入门小站

工具

虚拟化管理软件比较(Eucalyptus, OpenNebula, OpenStack, OpenQRM,XenServer, Oracle VM, CloudStack,ConVirt)

hanaper

强化学习RL AWS 自动驾驶DeepRacer ROS 架构 易筋 ARTS 打卡 Week 71

John(易筋)

ARTS 打卡计划

架构实战课程 模块6作业

Frank

这些行业用ERP系统会有很大帮助

低代码小观

企业管理 ERP

100台机器上海量IP如何查找出现频率 Top 100?

秦怀杂货店

IP 海量数据 top

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