C# 对类型系统扩展性的改进

前言

C# 对类型系统进行改进一直都没有停过,这是一个长期的过程。C# 8 之后则主要围绕扩展性方面进行各种改进,目前即将发布的 C# 11 中自然也包含该方面的进度。这些改进当然还没有做完,本文则介绍一下已经推出和即将推出的关于这方面改进的新特性。

接口

我们从最初的最初开始说起。

接口(interface)在 C# 的类型系统中是一个非常关键的部分,用来对行为进行抽象,例如可以抽象“能被从字符串解析为整数”这件事情的接口可以定义为:

interface IIntParsable
{int Parse(string text);
}

这样一切实现了该接口的类型就可以直接转换为 IIntParsable,然后调用其 Parse 方法把 string 解析成 int,用来根据字符串来创建整数:

class IntFactory : IIntParsable
{public int Parse(string text) { ... }
}

但是这样显然通用性不够,如果我们不想创建 int,而是想创建其他类型的实例的话,就需要定义无数个类型不同而抽象的事情相同的接口,或者将 Parse 的返回值改成 object,这样就能通用了,但是对值类型会造成装箱和拆箱导致性能问题,并且调用方也无法在编译时知道 Parse 出来的到底是个什么类型的东西。

泛型接口

为了解决上面的这一问题,C# 进一步引入了泛型。泛型的引入允许接口定义类型参数,因此对于上面的接口而言,不再需要为不同类型重复定义接口,而只需要定义一个泛型接口即可:

interface IParsable<T>
{T Parse(string text);
}

这样,当一个类型需要实现 IParsable<T> 时,就可以这么实现了:

class IntFactory : IParsable<int>
{public int Parse(string text) { ... }
}

由此,我们诞生了各式各样的工厂,例如上面这个 IntFactory 用来根据 string 来创建 int。基于这些东西,甚至发展出了一个专门的工厂模式。

但是这么做还有一个问题,假如我在接口中添加了一个新的方法 Foo,那么所有实现了这个接口的类型就不得不实现这个新的 Foo,否则会造成编译失败。

接口的方法默认实现

为了解决上述问题,C# 为接口引入了默认接口实现,允许用户为接口添加默认的方法实现。有了默认实现之后,即使开发者为一个接口添加了新的方法,只要提供一个默认实现,就不会导致类型错误而编译失败:

interface IParsable<T>
{T Parse(string text);public void Foo() { ... }
}

这样一来,IParsable<T> 就有 Foo 方法了。不过要注意的是,这个 Foo 方法不同于 Parse 方法,Foo 如果没有被实现,则不是虚方法,也就是说它的实现在接口上,而不会带到没有实现这个接口的类上。如果不给类实现 Foo 无法调用的,除非把类型强制转换到接口上:

class IntFactory : IParsable<int>
{public int Parse(string text) { ... }
}interface IParsable<T>
{T Parse(string text);public void Foo() { ... }
}var parser = new IntFactory();
parser.Foo(); // 错误
((IParsable<int>)parser).Foo(); // 没问题

接口的静态方法默认实现

既然接口能默认实现方法了,那扩充一下让接口支持实现静态方法也是没有问题的:

interface IParsable<T>
{T Parse(string text);public void Foo() { ... }public static void Bar() { ... }
}

不过,接口中的这样的静态方法同样不是虚方法,只有在接口上才能进行调用,并且也不能被其他类型实现。跟类中的静态方法一样,想要调用的时候,只需要:

IParsable<int>.Bar();

即可。

你可能会好奇这个和多继承有什么区别,C# 中接口的默认实现都是非虚的,并且还无法访问字段和不公开的方法,只当作一个向前兼容的设施即可,因此不必担心 C++ 的多继承问题会出现在 C# 里面。

接口的虚静态方法

将接口的静态方法作为非虚方法显然有一定的局限性:

  • 只能在接口上调用静态方法,却不能在实现了接口的类上调用,实用性不高

  • 类没法重写接口静态方法的实现,进而没法用来抽象运算符重载和各类工厂方法

因此,从 C# 10 开始,引入了抽象/虚静态方法的概念,允许接口定义抽象静态方法;在 C# 11 中则会允许定义虚静态方法。这样一来,之前的 IParsable<T> 的例子中,我们就可以改成:

interface IParsable<T>
{abstract static T Parse(string text);
}

然后我们可以对该接口进行实现:

struct Int32 : IParsable<Int32>
{public static int Parse(string text) { ... }
}

如此一来,我们组合泛型约束,诞生了一种全新的设计模式完全代替了原来需要创建工厂实例的工厂模式:

T CreateInstance<T>(string text) where T : IParsable<T>
{return T.Parse(text);
}

原来需要专门写一个工厂类型来做的事情,现在只需要一个函数就能完成同样甚至更强大的功能,不仅能省掉工厂自身的分配,编写起来也更加简单了,并且还能用到运算符上!原本的工厂模式被我们彻底扔进垃圾桶。

我们还可以将各种接口组合起来应用在泛型参数上,例如我们想编写一个通用的方法用来计算 a * b + c,但是我们不知道其类型,现在只需要简单的:

V Calculate<T, U, V>(T a, U b, V c)where T : IMultiplyOperators<T, U, U>where U : IAdditionOperators<U, V, V>
{return a * b + c;
}

其中 IAdditionOperators 和 IMultiplyOperators 都是 .NET 7 自带的接口,三个类型参数分别是左操作数类型、右操作数类型和返回值类型,并且给所有可以实现的自带类型都实现了。于是我们调用的时候只需要简单的 Calculate(1, 2, 3) 就能得到 5;而如果是 Calculate(1.0, 1.5, 2.0) 则可以得到 3.5

角色和扩展

至此,接口自身的演进就已经完成了。接下来就是 C# 的下一步计划:改进类型系统的扩展性。下面的东西预计会在接下来的几年(C# 12 或者之后)到来。

C# 此前一直是一门面向对象语言,因此扩展性当然可以通过继承和多态来做到,但是这么做有很大的问题:

  • 继承理论本身的问题:例如根据继承原则,正方形类型继承自长方形,而长方形又继承自四边形,但是长方形其实不需要独立的四边长度、正方形也不存在长宽的说法,这造成了实现上的冗余和定义上的不准确

  • 对类而言,只有单继承,没法将多个父类组合起来继承到自类上

  • 与值类型不兼容,因为值类型不支持继承

  • 对接口而言,虽然类型可以实现多个接口,但是如果要为一个类型添加新的接口,则需要修改类型原来的定义,而无法进行扩展

最初为了支持给类型扩展新的方法,C# 引入了扩展方法功能,满足了大多数情况的使用,但是局限性很大:

  • 扩展方法只能是静态方法,无法访问被扩展类型内部的私有成员

  • 扩展方法不支持索引器,也不支持属性,更不支持运算符

社区中也一直存在不少意见希望能让 C# 支持扩展一切,C# 8 的时候官方还实现了这个功能,但是最终在发布之前砍掉了。

为什么?因为有了更好和更通用的做法。

既然我们已经有了以上对接口的改进,我们何必再去给一个局限性很大的扩展方法缝缝补补呢?因此,角色和扩展诞生了。

在这个模式里,接口将成为核心,同时彻底抛弃了继承。接口由于自身的特点,在 C# 中也天然成为了 Rust 中 dyn trait 以及 Haskell 中 type class 的等价物。

注意:以下的东西目前都处于设计阶段,因此下述内容只是对目前设计的介绍,最终的设计和实现可能会随着对相关特性的进一步讨论而发生变化,但是总体方向不会变。

角色

一个角色在 C# 中可以采用如下方式定义:

role Name<T> : UnderlyingType, Interface, ... where T : Constraint

这样一来,如果我们想给一个已有的类型 Foo 实现一个有着接口 IBar 的角色,我们就可以这么写:

role Bar : Foo, IBar { ... }

这样我们就创建了一个角色 Bar,这个 Bar 则只实现了 IBar,而不会暴露 Foo 中的其他成员。且不同于继承,Foo 和 Bar 本质上是同一个类型,只是拥有着不同的角色,他们之前可以相互转换。

举一些现实的例子,假设我们有一个接口 IPerson

interface IPerson
{int Id { get; }string Name { get; }int Age { get; }
}

然后我们有一个类型 Data 使用字典存储了很多数据,并且 Data 自身具有一个 Id

class Data
{public int Id { get; }public Dictionary<string, string> Values { get; } =  ...;
}

那我们就可以给 Data 创建一个 Person 的角色:

role Person : Data, IPerson
{public string Name => this.Values["name"];public int Age => int.Parse(this.Values["age"]);
}

其中,无需实现 Id,因为它已经在 Data 中包含了。

最终,这个 Person 就是一个只实现了 IPerson 的 Data,它只暴露了 IdName 和 Age 属性,而不会暴露来自 Data 的 Values 属性。以及,它可以被传到任何接受 PersonData 或者 IPerson 的地方。

我们还可以组合多个接口来创建这样的角色,例如:

interface IHasAge
{int Age { get; }
}interface IHasName
{string Name { get; }
}role Person : Data, IHasAge, IHasName
{// ...
}

这样我们把 IPerson 拆成了 IHasAge 和 IHasName 的组合。

另外,在不实现接口的情况下,角色也可以用来作为类型的轻量级封装:

role Person : Data
{public string Name => this.Values["name"];public int Age => int.Parse(this.Values["age"]);
}

如此一来,Person 将成为一种提供以“人”的方式访问 Data 的方法的类型。可以说,角色就是对同一个“data”的不同的“view”,一个类型的所有角色和它自身都是同样的类型,在本质上和继承是完全不同的!与其他语言的概念类比的话,角色就等同于 concepts,这也意味着 C# 向 structural typing 迈出了一大步。

扩展

有了角色之后,为了解决扩展性的问题,C# 将会引入扩展。有时候我们不想通过角色来访问一个对象里的东西,我们可以直接在外部扩展已有的类型。

extension DataExtension : Data
{public string Name => this.Values["name"];public string ToJson() { ... }
}

这样,Data 类型就有了名为 Name 的属性和 ToJson 的方法,可以直接调用。除了属性和方法之外,扩展一个索引器自然也不在话下。

其中的 ToJson 类似以前的扩展方法,不过如此一来,以前 C# 的扩展方法特性已经彻底被新的扩展特性取代,而且是上位替代,功能性和灵活性上远超原来的扩展方法。

我们还可以给类型扩展实现接口:

extension DataExtension : Data, IHasName
{public string Name => this.Values["name"];
}

这样一来,Data 就实现了 IHasName,可以传递到任何接受 IHasName 的地方。

甚至借助接口的虚静态方法和泛型,我们可以给所有的整数类型扩展一个遍历器,用来按字节遍历底层的表示:

extension ByteEnumerator<T> : T, IEnumerable<byte> where T : unmanaged, IShiftOperators<T, T>
{public IEnumerator<byte> GetEnumerator(){for (var i = sizeof(T); i > 0; i--){yield return unchecked((byte)this >> ((i - 1) * 8));}}
}foreach (var b in 11223344556677L)
{Console.WriteLine(b);
}

配合接口的静态方法,我们甚至能给已有的类型扩展实现运算符!

extension MyExtension : Foo, IAdditionOperators<Foo, Foo, Foo>
{public static Foo operator+(Foo left, Foo right) { ... }
}var foo1 = new Foo(...);
var foo2 = new Foo(...);
var result = foo1 + foo2;

总结

C# 从 8 版本开始逐渐开始对接口进行操刀,最终的目的其实就是为了实现角色和扩展,改善类型系统的扩展性。到了 C# 11,C# 对接口部分的改造已经全部完成,接下来就是角色和扩展了。当然,目前还为时尚早,具体的设计和实现也可能会变化。

最终,借助接口、泛型、角色和扩展,C# 的类型系统将拥有等同于 Haskell 的 type class 那样的强大表达力和扩展性。而且由于是静态类型,从头到尾都不需要担心任何的类型安全问题。也可以预想到,随着这些特性的推出,将会有不少已有的设计模式因为有了更好的做法而被取代和淘汰。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/288043.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python 实现装饰器设计模式

python 装饰器简单、基本的实现并不复杂。装饰器&#xff08;Decorators&#xff09;模式类似于继承&#xff0c;当你需要为某一个对象添加额外的动作、行为时&#xff0c;在不改变类的情况下可以使用装饰器。这篇文就当做一篇水文&#xff0c;本来不想写&#xff0c;因为这个专…

Excel抽奖小程序

今天分享一个用Excel制作的抽奖小程序。 如上图&#xff0c;制作一个抽奖小界面&#xff0c;滚动显示区域写入“INDIRECT("A"&RANDBETWEEN(2,13))”&#xff0c;按F9键不放&#xff0c;程序开始运行&#xff0c;松开F9键&#xff0c;抽奖完成。 函数解说&#x…

python导入函数模块 为什么会打印两次_5.1.2Python从模块导入函数

Posted by 撒得一地 on 2016年3月2日 in python教程国外稳定加速器推荐vypr |NordPython下模块导入函数&#xff0c;即把某件事作为另一件事导入&#xff0c;从模块导入函数的时候&#xff0c;可以使用&#xff1a;import somemodule或者from somemodule import somefunction或…

lz98n外接电源注意问题

当外接电源时&#xff0c;要将跳线帽拔掉&#xff0c;外接电源- 接12v和gnd 此时还得注意 l298n5v变成的使能输入 必须是单片机的io5v输入 而且单片机的gnd要和l298n的gnd链接 也就是来l298N的GND 要接两根线 一个是外部电源的- 还有就是单片机的gnd 特别注意 l298n的5v不…

剑指offer之partition算法

1 问题 partition 算法: 从无序数组中选出枢轴点 pivot&#xff0c;然后通过一趟扫描&#xff0c;以 pivot 为分界线将数组中其他元素分为两部分&#xff0c;使得左边部分的数小于等于枢轴&#xff0c;右边部分的数大于等于枢轴&#xff08;左部分或者右部分都可能为空&#x…

Vagrant搭建可移动的PHP开发环境

准备 开发所需工具&#xff1a; VagrantOneinstackVirtualboxVagrant box系统环境&#xff1a;macOS Sierra 10.12.5搭建系统&#xff1a;CentOS 7搭建环境&#xff1a;Oneinstack&#xff08;PHP以及Java环境&#xff09; 为啥不用docker&#xff1f;因为很多公司用的windows&…

让DIV中文字换行显示

让DIV中文字换行显示 1. <style>div{white-space:normal;word-break:break-all;word-wrap:break-word; }</style><div style" width:100px; border:1px solid red">I am a doibiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii</div> 未加css前效果&…

C# numericUpDown控件用法总结及注意事项

numericUpDown控件在使用的过程当中,有些用法会不太一样,下面做一总结。 1. 判断numericUpDown的value属性是否为空 使用过Numericupdown控件的童鞋初期应该都会碰到一个奇怪的问题,在删除了控件里的值之后,里面实际上还是有数据的,所以也没办法判断非空了。 这里我觉得是…

WPF效果第一百八十三篇之无缝循环滚动

这不最近一直都在瞎玩Xamarin,渐渐的把WPF给冷落的;假期前突然收到一个着急的模糊不清的需求:图片无缝循环滚动;由于着急我就比较偷懒直接用了很low的方式实现了一版:1、前台就是直接Canvas嵌套StackPanel:<Canvas ClipToBounds"True" x:Name"RootCanvas&quo…

看得懂的外观设计模式 python3 实现

外观设计模式在平常的代码编写中&#xff0c;会经常使用。在平常代码的编写时&#xff0c;即使程序员没有从标准上认识过外观设计模式&#xff0c;但在开发的过程中&#xff0c;也会从代码的多方面角度考虑&#xff0c;从而编写了符合外观设计模式的代码。 很多程序员都有这种…

剑指offer之快速排序

1 快速排序 通过一趟排序将要排序的数据分割成独立的两部分&#xff0c;其中一部分的所有数据都比另外一部分的所有数据都要小&#xff0c;然后再按此方法对这两部分数据分别进行快速排序&#xff0c;整个排序过程可以递归进行&#xff0c;以此达到整个数据变成有序序列 2 分析…

机器学习------精心总结

1.数学 偏差与方差拉格朗日核函数凸优化协方差矩阵Hessian矩阵CDF&#xff08;累计分布函数&#xff09;高斯概率密度函数中心极限定理2.机器学习 Java 机器学习 工具 & 库 1.处理小数据效果好 2.深度学习—大数据&#xff0c;超过500w&#xff1b;图像&#xff0c;语言方…

mysql命令去重_MySQL去重的方法整理

{"moduleinfo":{"card_count":[{"count_phone":1,"count":1}],"search_count":[{"count_phone":4,"count":4}]},"card":[{"des":"阿里云数据库专家保驾护航&#xff0c;为用户…

NuGet包管理平台

这节来讲一下.NET下的包管理平台&#xff1a;NuGet。简介我们做一个项目&#xff0c;除了自己的代码文件之外&#xff0c;实际上还要引用诸多代码文件&#xff0c;这些文件可能是我们自己封装的底层框架代码&#xff0c;或者为了完成某个功能而引用的工具类文件等等。在.NET里边…

【ArcGIS风暴】ArcGIS影像批量裁剪(分幅)方法总结

实际工作中经常需要采用规则格网或标准分幅格网去对影像进行分幅。ArcGIS提供了强大的影像批量裁剪(分幅)的功能,常规的方法是利用掩膜提取工具手工重复裁剪,费时又费力,裁到让GISers怀疑人生。。。。。当然了如果你是个码农,会使用Python语言的话就很简单了。前面也有文…

Python变量的复制

Python变量的复制 dic {a: 1} dic_fake_copy dic dic_fake_copy.update({b: 2}) print dic_fake_copy %s % dic_fake_copy print dic %s % dic 输出结果为&#xff1a; In [6]: print dic_fake_copy %s % dic_fake_copy dic_fake_copy {a: 1, b: 2}In [7]: print dic %s…

看得懂的设计模式 享元模式python3 最基本(简单)实现

在考量系统内存合理使用时&#xff0c;通过享元模式可降低性能压力以及降低资源占用&#xff1b;主要实现是通过共享数据这一思想实现资源的合理分配。 在开发项目时&#xff0c;很多情况下会存在过多的相似对象&#xff0c;该对象有相同的共同点&#xff0c;该共同点在程序设…

剑指offer之最小的K个数

1 问题 输入N个整数&#xff0c;找出其中最小的K个&#xff0c;例如输入数组6、5、1、4、 2、 7、 3、 8&#xff0c;最小的4个数是1、2、3、4 2 分析 1&#xff09;我们可以用快速排序从小到大&#xff0c;但是时间复杂度是O(nlogn) 我们取出最前面的K个数就行。 2&#xf…

JCheckbox全选

在实际的使用过程中的一些小技巧。在图形界面的编程中&#xff0c;复选框一般是多个在一起&#xff0c;如果要进行全选时&#xff0c;则要将复选框全部设置setSelected为true&#xff0c;那么如果当前容器里面的复选框很多的时候怎么办呢&#xff0c;我们可以采用向下转型来完成…

creo管道设计教程_CREO/PROE产品设计教程之四芯花线建模,加深对关系式的认识...

阅读完&#xff0c;如果觉得有用&#xff0c;那么点击"关注"和点赞是对作者的一种尊重和鼓励。版权所有&#xff0c;抄袭必究。春节前&#xff0c;基本敲定和相关知名出版社在2020年的图书创作及出版计划。文&#xff1a;钟日铭我曾经介绍过三芯"花线"建模…