阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

未来的.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:002634
用户头像

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

关注

评论

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

【开源三方库】crypto-js加密算法库的使用方法

OpenHarmony开发者

OpenHarmony

制丝系统盘、启动盘

玄兴梦影

windows 系统重装 Liunx

Databend 数据集成方案 | Data Infra 第 15 期

Databend

航空航天行业的MES系统解决方案

万界星空科技

MES系统

WorkPlus AI助理,基于ChatGPT的企业级知识问答机器人

WorkPlus

研发管理平台的比较框架

iSoftBook

软件工程 敏捷精益 研发团队 研发效能管理 研发管理平台

从“13天”到“0天”延时,揭秘幸福里离线SLA保障最佳实践

字节跳动数据平台

大数据 数据中台 数据治理 数据安全 企业号9月PK榜

可观测性在灰度发布中的应用

观测云

微服务 性能优化 链路

智慧火力发电厂数字孪生3D可视化平台

2D3D前端可视化开发

智慧电厂 智慧火电厂 智慧火力发电厂 数字孪生火电厂 火电厂三维可视化

山东布谷科技直播APP源码搭建:核心的服务器系统

山东布谷科技

软件开发 系统架构 直播APP源码 服务器系统

产品路线图如何制定?斯坦福大学产品管理课程为你支招

LigaAI

产品经理 经验总结 产品管理 产品路线图 企业号9月PK榜

大模型驱动云计算创新变革

Baidu AICLOUD

异构计算 百度百舸 千帆大模型平台 LMops AI 原生

Apache Commons Daemon 使用教程

玄兴梦影

Linux windows 进程 服务

数据通信网络之IPv6基础

timerring

数据通信网络

弹性数据库连接池探活策略调研(三)——DBCP | 京东云技术团队

京东科技开发者

数据库 数据库连接池 企业号9月PK榜 DBCP

「程序员转型技术管理」必修的 10 个能力提升方向

LigaAI

开发者 技术管理 经验总结 技术领导力 企业号9月PK榜

学会PCB通用布局规则,复杂设计也能轻松搞定!

华秋电子

PCB

无目标,有目标,多目标

玄兴梦影

目标 前进的路上

超全60000多字详解 14 种设计模式 (多图+代码+总结+Demo)

控心つcrazy

小红书广告智能创意能力构建过程详解

小红书技术REDtech

人工智能 小红书

球场LED显示屏应具备什么特点

Dylan

广告 足球 LED LED display LED显示屏

Axios put 请求使用指南:优化开发流程

Apifox

程序员 前端 后端 axios put

金融行业首批!度小满智算网络中心达到万卡规模

科技热闻

HBuilder开发者必备!Windows上传IPA文件的软件分享

雪奈椰子

亚信安慧荣膺“信创工委会技术活动单位”

亚信AntDB数据库

AntDB 国产数据库 AntDB数据库

从融云数智办公平台,看企业需要什么样的大模型?

融云 RongCloud

人工智能 AI 互联网 AIGC 数智办公

Sketch for mac(矢量绘图UI设计软件) 98.1中文激活版

mac

Sketch 苹果mac Windows软件 矢量绘图软件

一见“氢”心-康士柏氢能产业链蓄力待发

Geek_2d6073

打造次世代分析型数据库(二):这,不只是列存表

腾讯云大数据

数据库

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