【AICon】探索RAG 技术在实际应用中遇到的挑战及应对策略!AICon精华内容已上线73%>>> 了解详情
写点什么

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

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

关注

评论

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

基于开源IM即时通讯框架MobileIMSDK:RainbowChat v8.4版已发布

JackJiang

即时通讯 即时通讯IM

直播 | StarRocks 实战系列第二期--导入优化&问题排查

StarRocks

数据库 开源

ChatGPT入门案例|商务智能对话客服(二)| 社区征文

TiAmo

openai ChatGPT

国际财务系统基于ShardingSphere的数据分片和一主多从实践

京东科技开发者

数据库 数据分片 ShardingSphere 企业号 2 月 PK 榜 一主多从

拥有了瓴羊Quick BI,企业的数据分析变得更好

巷子

利用DUCC配置平台实现一个动态化线程池

京东科技开发者

spring 多线程 代码 动态线程池 ducc

百分点科技宣布接入百度文心一言能力 数据科学基础平台获领先AI技术加持

百分点科技技术团队

程序员超级干货书单:技术人必看

引迈信息

程序员 敏捷开发 书单

使用自定义的初始化方法宏(OC)

刿刀

飞桨框架v2.4 API新升级!全面支持稀疏计算、图学习、语音处理等任务

百度Geek说

API 框架 3D点云 企业号 2 月 PK 榜 Sparse Transformer

可靠、稳定、安全,龙蜥云原生容器镜像正式发布!

OpenAnolis小助手

开源 容器 云原生 镜像 龙蜥社区

炸了!3年图片都没了

艾小仙

更专业、安全、可控!政企都选择WorkPlus私有化部署

WorkPlus

WorkPlus即时通讯集成工作平台,提效企业一体化管控

WorkPlus

黑龙江哈尔滨市等保测评机构有五家啦!名单看这里!

行云管家

等保 机构 等保测评 哈尔滨

使用了瓴羊Quick BI,数据分析的效率有效提升

夏日星河

大规模即时云渲染技术,追求体验与成本的最佳均衡

阿里云视频云

云计算 云渲染 云庙会

有了瓴羊Quick BI,企业再也不必担心可视化分析情况

小偏执o

QCon演讲实录(下):多云管理关键能力实现与解析-AppManager

阿里云大数据AI技术

大数据 运维 多云服务 多云管理

连续两年榜上有名!TDengine 荣获墨天轮“2022 年度时序数据库”奖项

TDengine

数据库 tdengine 时序数据库

瓴羊Quick BI可视化功能,满足企业的数据分析需求

对不起该用户已成仙‖

90%企业在探索的敏捷开发怎么做?极狐GitLab总结了这些逻辑与流程

极狐GitLab

DevOps 敏捷开发 持续集成 CI/CD 持续交付

Maven Shade插件relocation修改类常量的问题

Laughing

Java 后端 Maven-Shade-Plugin RelocationClass

什么是网关型堡垒机?与运维审计堡垒机有什么区别?

行云管家

堡垒机 堡垒机网络安全

完美主义者友好!合合信息旗下扫描全能王“智能擦除”照片中的杂物

合合技术团队

人工智能 图片 文本

电阻为什么都是4.7kΩ、5.1kΩ,而不是整数5kΩ?

元器件秋姐

科普 元器件 元器件知识 电阻 电阻值

多款社交黑马海外霸榜,融云全球通信服务护航登顶

融云 RongCloud

用Docker在本地搭建nginx静态服务器&反向代理

致知Fighting

Java Docker 后端

关系型数据库的架构演变

石臻臻的杂货铺

数据库 架构

MASA Stack 1.0 发布会 —— 社区问题解答

MASA技术团队

.net stack 应用现代化 MASA

飞桨特色产业级模型库助力AI开发与落地更简单

飞桨PaddlePaddle

paddle 开源 模型 飞桨

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