关于编程语言的可变性和不变性 - alexfertel


在本文中,我们将讨论可变性、它的含义以及在编程时与其相关的不同权衡。
什么是可变性?
可变值是可以在程序执行期间更改的值。这意味着我们可以创建一个值,将其绑定到一个变量,重新分配变量,或更改值的一部分。
请注意,我们谈论的是值而不是变量,因为它们并不相同。拥有可变值意味着在内存中拥有一个我们可以更改的对象。相比之下,拥有可变变量只是说我们对值的访问点是可变的。在实践中,可变性是由编译器为我们处理的。
另一方面,我们具有不变性,这意味着无法更改值/变量。我们在编程语言中经常发现的一些不可变值的例子是字符串和日期。读者可能会想:一个我不能改变的变量有什么用?好吧,这可能会让人感到惊讶,但有些编程语言的一切都是不可变的。
 
不变性好处
大多数时候,我们听到可变的这个和那个不变的,但没有人花时间解释为什么我们应该理解权衡:知道什么时候使用可变或一成不变的模式可以消除错误,使得整个类或使代码更易读和简洁。
我们可以在最低级别找到可变性。在汇编语言中,我们与内存交互,以我们想要的方式不受限制地对其进行变异。它是相关的,因为我们必须了解突变是一个高级概念。我们不讨论程序集级别的可变性:一切都是可变的。计算机的整个基础是将 0 变为 1。
在更高的层次上,有些编程语言我们不能强制执行不变性,但在特定情况下或内置类型中。例如,Python 无法从不可变的内置类型中做到这一点。
使用不可变值的一些好处:

  • 通常,不变性更有意义。当我们更改 value1或向年添加Date时,这些值不会对已经存在的修改,相反,我们完全有了新的值。
  • 制作不可变值的副本非常便宜且快速。我们可以返回相同的值,因为它不能改变程序的每个部分都可以引用这个特定的值。
  • 我们可以使用不可变值来节省内存。如果我们有 100 个字符串实例,它们可以更改为引用该字符串。
  • 不可变值本质上是线程安全的。无法更改消除了数据竞争的发生。

 
各种语言的可变性与不变性
一个例子是 Javascriptconst变量。关于是否使用 let 或const 一直存在很多争议,我们不会深入讨论。但是有一点很清楚,const 并不意味着不变性。最多,这意味着是不可变的(更像是只读的)。我们不能重新分配给用 声明const 的变量,但我们当然可以改变具有间接级别的值。
我们所说的间接是指引用是不可变的,但引用指向的值不是。这些变量真的是不可变的吗?
默认情况下,其他语言(如 C# 和 C++)具有可变值。但是,有一些编程结构和技术可以强制执行不变性。在 C# 中,我们可以有效地实现自定义的不可变类型,如Eric Lippert精彩的博客中优雅的展示 。
其他编程语言允许可变和不可变变量,如 F# 和 Rust。这两个默认都有不可变的变量,只有在添加某些关键字后,变量才有效地可变。值得注意的是,编译器会强制执行可变性。
最后,在 Haskell 和 Clojure 等语言中,所有值都是不可变的。这意味着无法在创建后改变值。
我们可以在更高的层次上找到这个概念:
以 ReactJS 为例。本段将纯函数的概念与不变性合并了一点,但我们将尽量保持在范围内。ReactJS 是一个用于构建用户界面的库。它的本质是 UI 是状态的函数。这听起来很抽象,但已经足够了。该库维护一些状态并呈现UI。我们可以说 render 意味着 绘制到浏览器,而 state 是由库管理的Javascript 变量。对我们来说相关的问题是状态是不可变的. 为了改变状态,我们使用库提供的更新程序函数,我们只能通过使用这个函数来改变状态。有了这个约束,ReactJS 保证确定性地构建 UI。
但是,规模仍然扩大。我们可以谈论不可变的基础架构,以及在不允许更改的地方部署软件的方法。相反,新部署取代了旧部署。非常像使每个部署不可变。
 
我们为什么要关心?
好的,所以我们知道我们可以在什么级别找到可变性,但我们为什么要关心?好吧,是否可变会改变程序的语义并产生影响。
在下面的代码片段中,whatAmI调用后 的值是sus什么?

const whatAmI = {foo:2021};
sus(whatAmI);
console.log(whatAmI);

我们必须检查代码sus才能知道变量的值。如果whatAmI是不可变的呢?然后,我们不会关心 的实现,sus因为我们知道它只是读取变量。 对于那些希望看到更多关于可变性如何令人担忧的例子的人来说,是一本很好的读物。
我认为权衡是不可变语义更难编写,但更容易调试和推理。
那么为什么不结合两全其美。这就是像 Rust 和 F# 这样的语言所做的。想象一下享受不变性带来的好处,同时可以在需要时选择退出。这是最好的场景吗?
let imutable_vector = vec![1,2,3,4];
imutable_vector.push(5);  //error

上面的代码片段将无法编译,因为该值是不可变的,我们正在尝试向向量添加一个元素。在这种情况下,解决方案是使用mut关键字使变量可变。

let mut mutable_vector = vec![1,2,3,4];
imutable_vector.push(5);