Logo
Section 1 / 3

不可变数据

熟悉函数式编程语言中不可变性的优势。

导入

不可变数据

熟悉函数式编程语言中不可变性的优势。

如果我们倾听函数式编程专家的意见,会发现他们非常强调不可变性——即在函数式程序中,数据一旦创建便无法被修改。事实上,Elixir 强制要求数据不可变。那么,为什么如此重要呢?

让我们暂时放下 Elixir,思考一下我们当前所使用的编程语言。假设我们写下这样的代码:

1count = 99 2do_something_with(count) 3print(count)

我们理所当然地期望输出为 99。如果结果不是这样,我们会感到非常惊讶。我们相信 count 的值永远是 99。当然,我们可以将变量绑定到新值,但这并不改变事实:数值 99 本身始终还是 99。

试想一个世界,在这个世界里我们无法依赖这一点——某些其他代码,甚至可能与我们的代码并行运行,可以随意更改数值 99 的值。在这个世界中,调用 do_something_with 可能触发一段后台运行的代码,并将值 99 作为参数传入。而这段代码可能会修改接收到的参数内容。于是,原本的 99 突然变成了 100。

这显然令人不安,更糟糕的是,我们根本无法保证代码始终产生正确的结果。

回到我们当前的语言,再来看下面这段代码:

1array = [ 1, 2, 3 ] 2do_something_with(array) 3print(array)

我们依然希望 print 调用输出 [1,2,3]。但在大多数语言中,do_something_with 接收到的是数组的引用。如果它决定修改第二个元素,或者清空整个数组内容,那么最终输出就不再是预期的结果了。此时,我们很难仅通过阅读代码来准确判断其行为。

再进一步设想:多个线程同时访问这个数组。当它们都开始修改数组时,我们根本无法预知数组最终处于什么状态。

在许多编程语言中,复合数据结构通常是可变的,意味着我们可以修改其全部或部分的内容。如果多段代码在并发环境下对这些数据进行修改,问题就会变得复杂且难以控制。

Elixir 中的不可变性

Elixir 避开了这些问题。在 Elixir 中,所有值都是不可变的。一旦一个变量指向某个列表如 [1,2,3],我们就知道它将始终指向这些相同的值(直到我们重新绑定该变量)。这种特性让并发编程变得简单得多、安全得多。

但如果我们需要将 [1,2,3] 中每个元素都加上 100 呢?Elixir 的做法是生成一个包含新值的副本。原始数据保持不变,而我们的操作也不会影响任何其他持有原始数据引用的代码。

这很好地契合了编程的本质:数据转换。当我们更新 [1,2,3] 时,并不是直接在原地修改它,而是将其转化为一个新的数据结构。

模式匹配

不可变性的性能影响