写点什么

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

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

关注

评论

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

【论文速读】| 大语言模型平台安全:将系统评估框架应用于OpenAI的ChatGPT插件

云起无垠

Sermant热插拔能力在故障注入场景的实践

华为云开源

开源 微服务 服务治理

LangChain初探:为你的AI应用之旅导航

蛋先生DX

#人工智能 LLM #LangChain Prompt 企业号2024年4月PK榜

人工智能,应该如何测试?(二)数据挖掘篇

霍格沃兹测试开发学社

做跨境电商,为什么要建独立站

Noah

BSN-DID研究--主题二:发证方函数

BSN研习社

区块链 BSN did

我们是如何测试人工智能的(四):模型全生命周期流程与测试图

测试人

人工智能 软件测试

VMware ESXi 8.0U2b macOS Unlocker & OEM BIOS 标准版和厂商定制版

sysin

esxi 驱动 unlocker dell hpe

VMware ESXi 8.0U2b macOS Unlocker & OEM BIOS 集成网卡驱动和 NVMe 驱动 (集成驱动版)

sysin

esxi 驱动 网卡 BIOS unlocker

2024年LED显示屏租赁屏市场

Dylan

商业 LED显示屏 全彩LED显示屏 led显示屏厂家 舞台表演

AMA live class

EchoZhou

English

思考-使用JSON结构映射业务数据与数据库表结构

alexgaoyh

json 数据库 系统设计 映射

Sermant热插拔能力在故障注入场景的实践

华为云开发者联盟

开源 华为云 华为云开发者联盟 sermant 企业号2024年4月PK榜

支付系统概述(五):结算系统

agnostic

支付系统设计与实现

Advanced RAG 02:揭开 PDF 文档解析的神秘面纱

Baihai IDP

AI LLM 白海科技 企业号 4 月 PK 榜 检索增强生成

效率提升 80%:go-mongox 让复杂的 BSON 数据编写变得简单

陈明勇

Go 开源 go mongo

基于开源IM即时通讯框架MobileIMSDK:RainbowChat-iOS端v9.0版已发布

JackJiang

网络编程 即时通讯 IM

Overlay网络与Underlay网络:深入探索与全面对比

GousterCloud

网络 #Kubernetes#

OpenAI Sora:60s超长长度、超强语义理解、世界模型。浅析文生视频模型Sora以及技术原理简介

蓉蓉

openai GPT-4 人工智

人工智能,应该如何测试?(三)数据构造与性能测试篇

霍格沃兹测试开发学社

广州等级保护测评公司一览表2024

行云管家

等保 堡垒机 等级保护 等保测评

云服务器干嘛的?带你掌握云计算的优势

一只扑棱蛾子

云服务器

行云防水堡-打造企业数据安全新防线

行云管家

网络安全 数据安全 防水堡

如何打造全国一体化算力体系?

天津汇柏科技有限公司

算力 一体化

探索Kubernetes的大二层网络:原理、优势与挑战🚀

GousterCloud

大二层网络 网络模型 #k8s

阿里巴巴中国站按关键字搜索商品 API接口使用指南:快速获取商品ID、名称、描述、价格

技术冰糖葫芦

API Explorer API 文档

人工智能,应该如何测试?(四)模型全生命周期流程与测试图

霍格沃兹测试开发学社

【荣誉】第七在线出席ToB商业头条行业大会 斩获创新力产品奖

第七在线

@Transactional事务是真的好用吗

派大星

Spring事务 Java 面试题 互联网大厂面试

Kubernetes大二层网络:挑战与解决方案探索

GousterCloud

cni #k8s

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