QCon北京「鸿蒙专场」火热来袭!即刻报名,与创新同行~ 了解详情
写点什么

未来的.NET 之多重继承

  • 2017-04-18
  • 本文字数:4118 字

    阅读完需:约 14 分钟

通过抽象接口引入有限形式的多重继承,这一.NET 新提议颇具争议性。该特性是受 Java默认方法(Default Methods)的启发。

默认方法的目的在于允许开发人员修改已发布的抽象接口。修改已发布接口将会产生破坏性的更改,因此在 Java 和.NET 中通常是不允许的。默认方法的提出,为接口编写者提供了一种可重写的实现,缓解了向后兼容问题。

在 C#版本的提议中,将包括用于如下部分的语法:

  • 方法体(即“默认”实现);
  • 属性访问器体;
  • 静态方法和属性;
  • 私有方法和属性(默认访问是公开的);
  • 覆写方法和属性。

这个提议并不允许接口具有域,因此形式上是一种有限的多重继承,但避免了一些已在 C++ 中发现的问题(尽管域可以使用 ConditionalWeakTable 和扩展属性模式模拟)。

用例:IEnumerble.Count

IEnumerable<T>添加 Count 方法是该特性最广为使用的用例。具体做法并非使用Enumerable.Count这一扩展方法,而是开发人员可以免费获取Count方法,并且如果开发人员能够提供更高效实现的话,能够可选地(optionally)覆写该方法:

复制代码
interface IEnumerable<t>
{
int Count()
{
int count = 0;
foreach (var x in this)
count++;
return count;
}
}
interface IList<t> ...
{
int Count { get; }
override int IEnumerable<t>.Count() => this.Count;
}
</t></t></t>

正如从上例中可以看到的,实现IList<T>的开发人员无需担心覆写IEnumerable<T>.Count()方法,因为它将自动获得IList<T>.Count

大家所关注的一个问题是该提议会使接口膨胀。既然可以在IEnumerable中添加Count方法,为什么不能添加其他所有的IEnumerable扩展方法?

Eirenarch 这样写道:

有人会认真考虑将Count()添加到IEnumerable,对此我有点吃惊。这不是和Reset方法同样的问题吗?并非所有的IEnumerable都可重置,或是可安全地做计数,因为其中的一些接口是一次性的。现在看这个问题,我想不起曾在IEnumerable上使用过Count(),只是在数据库 LINQ 调用中使用过,因为我不想冒险让 Count() 消费可枚举类型,或是变得低效。为什么要鼓励更多的Count()

DavidArno 补充道:

哈哈,很高兴能看到对这一提议的争论。基础类库(BCL,Base Class Library)团队早就将各种集合类搞得混杂不堪。就这一点而言,我怀疑团队中是否有人真正考虑过 Barbara Liskov 的建议,她所提出的替换原则如此完全地被打破了。如果将这一提议中的理念赋予团队,这将允许他们造成更大的破坏。想想就十分可怕!

在一次 BCL 会议上:

“OK,各位,我们想让IEnumerable<T>接口支持 cons 功能。大家有何建议?”

“这很简单。默认接口方法就能为我们解决这个问题。只需加入(T head, IEnumerable<T> tail) Cons() => throw new NotImplementedException(),这就完事了。IEnumerable 的实现者完全可以在闲暇时添加这一实现。”

“非常好,搞定。谢谢大家,本周会议结束。”

注意,LINQ 是由另一个独立团队负责的。LINQ 的功能并没有计划要切实地迁移到 IEnumerable 中。

这一更改也会打破当前扩展方法所提供的层次。目前Enumerable.Count方法位于 System.Core 动态库中,比 mscorlib 动态库要高两层。可能有人认为将 LINQ 的部分或完全地加入 mscorlib 中,会造成该动态库没有必要的膨胀。

另一个批评意见认为这一提议是没有必要的,因为已经存在允许可选地覆写扩展方法的设计模式。

可覆写扩展方法模式

可重新扩展方法依赖于接口检查。理想情况下只需要对一个接口做检查。但是由于一些历史遗留问题,以Enumerable.Count为例,需要检查两个接口。代码如下:

复制代码
public static int Count<tsource>(this IEnumerable<tsource> source) {
var collectionoft = source as ICollection<tsource>;
if (collectionoft != null) return collectionoft.Count;
var collection = source as ICollection;
if (collection != null) return collection.Count;
int count = 0;
using (var e = source.GetEnumerator()) {
while (e.MoveNext()) count++;
}
return count;
}
</tsource></tsource></tsource>

(为清楚起见,例子中移除了错误处理的代码。)

这个模式的缺点是存在可选接口过于宽泛的问题。例如,如果在类中想要覆写Enumerable.Count方法,那么需要实现整个ICollection<T>接口。对于只读类,则要编写大量的 NotSupported 异常(重申一下,这里由于历史原因要查看的是 ICollection<T>接口,而非更小的IReadOnlyCollection<T>接口)。

默认方法,类的公有 API

为了在添加新方法时,为避免向后兼容性问题,不能通过类的公有接口访问默认方法。以IEnumerable.Count为例,看下面的类:

复制代码
class Countable : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator() {…}
}
</int></int>

鉴于并未覆写IEnumerable.Count方法,因此不能这样编写代码:

复制代码
var x = new Countable();
var y = x.Count();

而是需要做类型转换:

var y = ((IEnumerable<int>)x).Count();</int>这时为实现在类的公有 API 上暴露接口方法,需要添加样板代码。这样的做法限制了对类提供默认实现这一技术的有用性。

使用一个默认方法覆写另一个默认方法

一个接口中的默认方法可以覆写另一个接口中的默认方法。这一点可以在IEnumerable.Count用例中看到。

正常情况下,需要对方法显式地指定override关键字,否则新方法在处理上将与其它方法无关。

也可以将一个接口方法标记为“override abstract”。通常没有必要指定abstract关键字,因为所有的抽象接口方法默认就是abstract的。

扩展方法与默认参数的解析顺序

Zippec 提出了一个重要问题,即如果新添加的接口方法与用于该接口的扩展方法命名相同时,将会发生什么:

将当前 API 升级为默认方法时会发生什么?我是否可以认为它们应该比扩展方法在覆写解析上具有更高的优先级?让我们以Count()方法为例。我们可以从IEnumerable上得到该方法吗?如果是这样,它将隐藏使用该特性在 C#中重新编译后的 LINQ IEnumerable.Count()实现,这是否会更改被调用的代码?我认为对于IQueryable,这是一个问题。

如果该问题存在,为缓解该问题,我们在 BCL 中以属性方式得到Count方法。由于默认方法会更改任何自定义扩展方法的现有实现,这是否意味着在已经存在的 BCL 接口上,我们永远无法获得任何默认方法(只能获得属性)?

尽管不常见,一些开发人员的确创建了自己的扩展方法库,镜像了 LINQ 中的库,但是具有不同的行为。如果扩展方法是作为默认方法移入接口中的,那么就会失去置换扩展库的能力。

用例:INotifyPropertyChanged

下面给出了另一个用例,一般人们在考虑新特性时可能使用:

复制代码
interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(args);
protected void OnPropertyChanged([CallerMemberName] string propertyName) => OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
protected void SetProperty<t>(ref T storage, T value, PropertyChangedEventArgs args)
{
if (!EqualityComparer<t>.Default.Equals(storage, value))
{
storage = value;
OnPropertyChanged(args);
}
}
protected void SetProperty<t>(ref T storage, T value, [CallerMemberName] string propertyName) => SetProperty(ref storage, value, new PropertyChangedEventArgs(propertyName));
}
</t></t></t>

但是,该用例并不会真正工作,因为接口没有提供生成事件的方法。接口只是定义了事件的 Add 和 Remove 方法,没有定义用于存储事件句柄列表的底层代理。

在提议中并未考虑这一问题,因此该问题是可以更改的。通用语言运行平台(CLR)的确为存储事件的“生成 Accessor 方法”预留了位置,虽然当前仅能使用 VB 语言。

更多支持的声音

HaloFour 写道:

这看上去非常像是一个意识形态上的争论。其中有一些已知的问题自发布.NET 1.0 以来,就一直没有被团队很好的解决。长期以来,标准解决方案一直是摆在那里的,但是这些方案常将 API 弄得完全一团糟,给出了 IFoo、IFoo2、IFoo3、IFooEx、IFooSpecial、IFooWithBar 这样的内容。扩展方法为解决这些问题做了大量工作,但是局限于那些可以在扩展方法中明确识别和分发的问题。除此之外,扩展方法缺乏特化(Specialization)。

默认实现很好地解决了这些问题。它允许 Java 团队使用额外的帮助方法(Helper Method)覆写在 Java 中已长期存在的接口,其中一些的确通过各种实现得以特化,例如Map#computeIfPresent

其它一些批评的声音

HerpDerpImARedditor 写道:

噢,该提议会引发那些积习难改的面条式代码(Spaghetti Code)。可能我考虑不周全,敬请谅解,但是这个模式解决了哪些在实现层无法解决的问题?这样的提议看上去抹煞了接口与具体实现之间存在的华丽差异。是否要让 IDE 完全指定运行时的出处?我不能认为这能与控制反转(IoC)一起工作。

当然,我十分热爱.NET,我历经了从经典的 ASP/VB 开发背景直至.NET 1。这是第一个我所反对的语言规格添加(虽然当“dynamic”登场亮相时,我在看到它的用例后的确在立场上做了一些让步)。虽然我看见一些人说他们将会忽略存在这一特性,但是我关注的是,可能今后我会在其他人的代码中碰上这样的特性,所以不能无视它。

当然还好,我猜测在任何真正的判决被通过前,我们不会看到这样的特性起作用。

Canthros 写道:

这让我很沮丧。

从 Github 上的讨论看,LINQ 的各种扩展方法所实现的一团糟引发了一些不满,尤其是为提供优化实现所必需完成的类型检测(type-sniffing)。虽然这一特性概化到语言特性中可能会大大降低.NET Core 实现者的工作,但付出的代价是语言需要承受接口和抽象类之间区分混乱,并且特性中大量吸入了下游存在的问题。

看起来 Shapes 提议可以大大缓解这个问题,但是现在我无暇切实考虑全部的问题。

查看英文原文: .NET Futures: Multiple Inheritance


感谢张卫滨对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-04-18 19:003013
用户头像

发布了 227 篇内容, 共 77.9 次阅读, 收获喜欢 28 次。

关注

评论

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

让算力普惠、释放技术红利,阿里云让开发者成为创新主体

阿里巴巴云原生

阿里云 Serverless 云原生 函数计算

【Linux】之【网络】相关的命令及解析[ethtool、nload、nethogs、iftop、iptraf、ifstat]

A-刘晨阳

Linux 网络 三周年连更

从初学者角度聊一聊socket到底是什么?

会踢球的程序源

Java 后端 socket

Golang负载均衡器Balancer的源码解读

骑牛上青山

Go 负载均衡

fabric.js开发图片编辑器可以实现哪些功能?多图

秦少卫

h5编辑器 FabricJS Fabric.js 海报编辑器 图片编辑

云效AppStack--扫雷亲测

六月的雨在InfoQ

云效 AppStack 云效流水线 Flow 三周年连更

更专业、安全的过等保,华为云等保合规解决方案值得选择

IT科技苏辞

分布式事务的21种武器 - 1

俞凡

架构

工赋开发者社区 | MES/MOM数据采集系统需求分析和总体设计

工赋开发者社区

助力企业网络安全建设,华为云等保合规解决方案值得拥有

路过的憨憨

2023移动云大会重磅官宣,云改“新三年”强势开局引期待?

ToB行业头条

华为云等保方案,轻松满足企业等保合规要求

IT科技苏辞

Mac怎么创建txt文件?如何设置新建txt的快捷键?

互联网搬砖工作者

布隆过滤器的设计之美,后端程序员一定要好好体会

程序员小毕

程序员 数据结构 面试 后端 布隆过滤器

Amazon 中国区配置 PingIdentity 身份集成实现 Redshift 数据库群集单点登录

亚马逊云科技 (Amazon Web Services)

机器视觉公司,在玩一局玩不起的游戏

脑极体

CV

劲爆!阿里巴巴面试参考指南(嵩山版)开源,程序员面试必刷

做梦都在改BUG

Java 程序员 面试

2023最NB的JVM基础到调优笔记,光图文就超清晰,吃透阿里P6小case

Java你猿哥

Java JVM Java虚拟机 jvm调优

如果有一天当你的Redis 内存满了,该怎么办?

会踢球的程序源

Java redis 后端

🔥🔥🔥热乎的前端面试题(昨天)

沉浸式趣谈

JavaScript 面试 Vue 前端面试

一天吃透操作系统八股文

程序员大彬

面试 操作系统

在华为云构建多云跨云的容灾系统,真的很香

路过的憨憨

2023年超全前端面试题-背完稳稳拿offer(欢迎补充)

肥晨

三周年连更

阿里巴巴灵魂一问:说说触发HashMap死循环根因

会踢球的程序源

hashmap Java1

【Java技术专题】「盲点追踪」突破知识盲点分析Java安全管理器(SecurityManager)

码界西柚

Java 安全管理器 SecurityManager

termius使用ssh教程 【XShell的神器Termius】

互联网搬砖工作者

多线程&高并发(全网最新:面试题+导图+笔记)面试手稳心不慌

Java你猿哥

Java 多线程 面试题 高并发 多线程与高并发

CNStack 云服务&云组件:打造丰富的云原生技术中台生态

阿里巴巴云原生

阿里云 云原生 CNStack

测试需要写测试用例吗?

老张

软件测试 质量保障 测试用例

限时开源!阿里京东架构师出品亿级高并发系统设计手册

会踢球的程序源

Java 架构 后端 java架构师

WebGPU 令人兴奋的 Web 发展

devpoint

WebGL webgpu #WebGPU 三周年连更

未来的.NET之多重继承_.NET_Jonathan Allen_InfoQ精选文章