C#的未来:不可变类

ngmm 9年前

英文原文:C# Futures: Immutable Classes

本文是 C# 的未来系列文章的最后一篇了,这次我们将讨论第 159 号提案,它建议在编译器中加入对不可变类的支持。虽说在 C# 中创建不可变类型一直以来都是可以做到的,并且C# 6 还将进一步简化这一过程,但目前还没有一种方式能够“将类声明为不可变”,并让编译器对这一声明进行校验。

这一提议看起来似乎并非十分重要,因为对类进行手动检查也不是非常困难的事。但如果缺少了对于不可变性的某种声明,就难以了解开发者的意图。应 用程序的开发者可能会做出某些假设,例如可以在多线程环境中安全地使用某个类,却发现在下一个版本中,类库的开发者为其加入了某个属性的 setter 方法,或是其它任何一种可变的、非线程安全的特性。

第 159 号提议推荐通过使用一个“immutable”关键字或者属性,显式地表明某个类型在任何情况下都不能更改。此外,一个不可变对象只能够引用其它不可变对象。

不可变对象的构造函数也具有一些限制,尤其是它们不能够通过“this”变量调用方法,因为这有可能将该对象在构造过程完成之前“泄漏”出去,从而破坏了对不可变性的承诺。至于这种泄漏应当引起一个错误还是一次警告,这一点可以再议。Sam Harwell 写道:

我比较倾向于发出警告的做法,虽然这种做法并不常见,并且也不是推荐的编码方式,但也很难肯定地说绝对没有人需要编写这样的代码。

不可变性与 Pure

第一眼看上去,不可变性与 Pure(纯对象或方法)约束的作用似乎是相同的,但它们之间确实存在着一些重要的不同之处。

  • 一个纯对象可以引用其它并未标注为 Pure 的对象,而正如之前所说,不可变类型不允许引用可变类型。
  • 纯方法不允许进行任何可见的状态更改,包括对当前对象与任何参数的更改。
  • 纯方法或纯属性可以更改内部状态。举例来说,它可以将某个计算的结果进行缓存以便重用。这一点并不违反纯方法的承诺,因为外部的观察者不会察觉到内部的变化。而不可变对象则没有这种能力。
  • 不可变对象中的方法只保证不会更改对象的状态,但允许对输入参数的状态进行更改。

考虑到这些不同之处的存在,相信你会看到许多同时具有不可变性与 Pure 特性的对象。

泛型与不可变性

泛型类型也将支持不可变性。要实现这一点,通常需要为每个类型参数都设定一个不可变性的限制。但这一条规则还有待商榷,有人认为可以直接从类型的参数中推断出不可变性,而无需显式地将其列为不可变。

欺骗

在某些情况下,你必须对类型系统进行欺骗,这一点已经得到了认可。打个比方,ImmutableArray 是对一个普通数组的封装。而如果按照这条提议的基本原则来说,这种行为是不允许出现的。为了处理这种场景,你可以在这个类的标注中进行声明,表示你是有意 地违背了这条原则,并且已经仔细地考虑过这样做的后果。这种做法与“unsafe”关键字的行为很相似,因为后者也是为了这种场合才出现的。

由于“unsafe”关键字的意思已经固定了,因此该提议考虑使用其它的关键字。目前来说,得到最多认可的关键字是“mutable”,不过看起来大家对此都不是十分满意。

只读性 —— 隐式或显式

在一个不可变对象中,每个字段在语义上都是只读的。某些开发者认为不必显式地进行声明,这可以减少代码的冗长度。另一些人则认为,正如在静态类中声明静态字段一样,每个字段都应该显式地标注为只读。

内部或外部校验

具体在何处强制不可变性,这一点需要认真考虑。某些人认为,正如代码契约与 Pure 属性一样,不可变性也应当由某个外部分析器进行处理。这样一来,开发团队就能够自行决定是否需要以编程方式强制不可变性。

另一部分开发者则认为,正因为如此,不可变性更不应当由外部工具进行处理。他们希望编译器能够进行不可变性的检查,这样就不会在无意中破坏了它的不可变性。

来自于微软的 Jared Parsons 写道:

我认为在这种场合使用分析器是错误的方案,在一个单一的 C# 项目中,要强制一系列规则的应用,甚至是对某种 C# 的变体进行分析,分析器都是一种优秀的选择。因为我对编译过程具有掌控权,因此可以随意地选择并使用分析器。

而如果需要在多个项目中强制某些规则,尤其是在这些项目属于不同开发者的情况下,分析器的作用就降低了。没有什么机制能够强制对某个项目引用通过某种分析器进行扫描,这种情况下唯一有效的强制措施就是双方的携手合作了。

这种情况与 Pure 属性的承诺不同,后者只是表示在某个对象中的任何方法或是属性都不会“产生任何可见的状态更改”。

来自: InfoQ