C# 的未来:不可变变量

  • Jonathan Allen
  • 邵思华

2015 年 5 月 6 日

话题:C#语言 & 开发

在 C# 中,readonly 关键字只能作用于字段级别。而在第 115 条提议 “只读本地变量与参数”中,将对 readonly 关键字进行扩展,以涵盖更广泛的场景。

提议中首先提出了创建只读本地变量的功能。这种功能的第一个使用场景只是用于文档,通过将某个变量标记为只读,就意味着在函数中的其它地方不能够、也不应该改动这个本地变量。对于代码很长、非常复杂,无法一眼看清所有情况的函数来说,这一点显得尤为实用。

第二个使用场景是在进行多线程运算和使用闭包时保证安全性。如果你通过执行 Parallel.ForEach 产生了一个闭包,则很容易会导致竞态的产生。而如果你默认将所有本地变量标为只读,那么那些可变的本地变量将显得很突出,因此在审查中就会注意到它。

语法上的困扰

如果能够默认使用只读本地变量,那么它确实能够带来许多价值,但这也要求语法不能太过繁重。考虑一下以下代码:

var gravity = 9.780327;

double gravity = 9.780327;

const double gravity = 9.780327;

虽然 gravity(重力)理应作为一个常量,但多数开发者都倾向于使用第一种代码版本。他们之所以没有选择“正确的做法”,只是为了简化输入,并且在一个独立的场景中,选用哪种方式其实无关紧要。

这种简便性胜于正确性的倾向也出现在类型转换中,下面的代码有一个常见的错误,许多有经验的程序员也难以避免。

var button = sender as Button;

button.Enabled = false;

正确的方法应该是“button = (Button)sender”,但这种正确的代码在输入时的复杂性稍高。

为了应对这些困扰,该提议提出了一种隐式类型化本地变量的简写方式,目前所考虑的有以下两种关键字:

val gravity = 9.780327;

let gravity = 9.780327;

在这两者之间,目前更倾向于“let”这一写法,因为在 LINQ 表达式中已经用到过它,并且从视觉角度来看更容易与“var”区别开(对于那些母语中不区分 r 与 l 的非英语使用者来说,后者也会更好)。

只读参数

接下来就是将参数标注为只读的功能。使用 Visual Studio 代码分析工具的开发者可能认为这一功能有些多余,因为该工具会自动阻止对常规的参数进行改变。但有些用例是它无法涵盖的。

在开发注重高性能的代码时,使用结构体替代类的做法并不罕见,即使结构体有时显得更大。为了避免结构体复制带来的消耗,因而会使用 ref 参数的方式将这些结构体传递给函数。

从文档的角度来看,这种函数签名无法清晰地向调用者表现出该参数不应被修改的意图。而将参数标注为“readonly ref”这种方式就能够填补这一漏洞。

并非每个人都喜欢这一语法,因为它要求使用“ref”装饰调用端,而这会产生某些误导的倾向。因此 Porges 建议使用一种“in”关键字予以代替。

void DoSomething(readonly ref LargeStruct value)

DoSomething(ref myLocal);

void DoSomething(in LargeStruct value)

DoSomething(myLocal);

Readonly 与 Const

虽然 readonly 能够彻底地避免某个值被替换,但它通常无法避免对某个对象的成员的改动。因此这条提议另外附加了一点,即对 readonly 所提供的保护能力进行扩展。

像 C++ 这样的语言已经能够支持这一概念了。虽然它确实能够像所说的方式一样工作,但要正确地使用它并不是一件容易的事,因为开发者们经常对于 const 这个关键字是对应变量本身,还是对应其中的内容感到困扰。因此在此语法中避免这种含糊性也是同样重要的。一种建议认为,可以使用“readonly”表示对变量本身的保护,而使用“const”表示对其内容的保护。

C++ 中还提供了 const 函数的概念,这种函数只能由定义为 readonly 或 const 的变量进行调用,因为这些变量已经证实了不会改变对象的状态。在.NET 中也能够通过 Pure 属性实现这一概念,但当前的 C# 编译器都不支持这一属性。

脚注:Readonly、结构体与字段

虽然并非提议中的一部分,但也应当考虑 readonly、结构体与字段的交互。考虑以下代码:

private readonly Foo _foo = new Foo(1, 2, 3);

如果 Foo 类型是完全不可变的,那么 readonly 字段就能够像预期一样工作。但如果 Foo 中提供了任何属性的 setter,那么每次你读取这个字段时,编译器会生成一个拷贝,所以你不会在无意中修改了它的内容。因此与 readonly 引用字段不同,readonly 结构体真正做到了只读。但由于这种方式带来了隐藏的性能损耗以及不一致性,因此如果能够以一种更清晰的方式表达出这一概念,将具有很大的益处。

要了解更多信息,请阅读 Eric Libbert 的帖子“Mutating Readonly Structs”。

查看英文原文:C# Futures: Immutable Variables

C#语言 & 开发