我目前正在开发React JS和React Native框架。在半路上,当我读到Facebook的Flux和Redux实现时,我遇到了Immutability或Immutable-JS库。

问题是,为什么不变性如此重要?改变对象有什么错?这不是让事情变得简单了吗?

举个例子,让我们考虑一个简单的新闻阅读器应用程序,它的打开屏幕是一个新闻标题的列表视图。

如果我设置一个带有值的对象数组,一开始我不能操作它。这就是不可变原理,对吧?(如果我说错了请指正。) 但是,如果我有一个新的News对象需要更新怎么办?通常情况下,我可以将对象添加到数组中。 在这种情况下我该如何实现呢?删除存储并重新创建它? 向数组中添加对象难道不是一种成本较低的操作吗?


当前回答

问题是,为什么不变性如此重要?改变对象有什么错?这不是让事情变得简单了吗?

关于可变性

从技术角度来看,可变性并没有错。它是快速的,它是重复使用内存。开发人员从一开始就习惯了它(我记得)。可变性的使用存在问题,也会带来一些麻烦。

如果object不与任何东西共享,例如存在于函数的作用域中并且不对外公开,那么就很难看到不变性的好处。在这种情况下,不可变是没有意义的。一成不变的感觉始于某些东西被分享。

可变性头痛

可变的共享结构很容易产生许多陷阱。对访问引用的代码的任何部分的任何更改都会影响到具有此引用可见性的其他部分。这种影响将所有部分连接在一起,即使它们不应该意识到不同的模块。一个函数的突变可能会导致应用程序的完全不同部分崩溃。这样的事情是一个糟糕的副作用。

其次经常与突变的问题是损坏的状态。当突变过程中途失败,一些字段被修改,一些字段没有被修改时,就会发生损坏状态。

更重要的是,通过突变很难追踪变化。简单的参考检查不会显示出差异,要知道发生了什么变化需要做一些深入的检查。此外,为了监测变化,还需要引入一些可观察的模式。

最后,突变是信任缺失的原因。如果某个结构可以变异,你怎么能确定它有你想要的价值。

const car = { brand: 'Ferrari' };
doSomething(car);
console.log(car); // { brand: 'Fiat' }

如上例所示,传递可变结构总是可以通过具有不同的结构来完成。函数doSomething正在改变外部给定的属性。没有对代码的信任,你真的不知道你拥有什么,你将拥有什么。所有这些问题的发生是因为:可变结构表示指向内存的指针。

不可变性与价值有关

不可变性意味着改变不是在相同的对象、结构上完成的,而是在新的对象、结构中表示的。这是因为引用不仅表示内存指针的值。每一次改变都会创造新的价值,而不会改变旧的价值。这样清晰的规则给予了信任和代码的可预测性。函数使用起来是安全的,因为它们处理的不是突变,而是具有自己值的自己的版本。

使用值而不是内存容器可以确定每个对象都表示特定的不可更改的值,并且使用它是安全的。

不可变结构表示值。

我将在一篇中型文章(https://medium.com/@macsikora/the-state-of- immutabil-169d2cd11310)中更深入地探讨这个主题

其他回答

我认为支持不可变对象的主要原因是保持对象的状态有效。

假设我们有一个叫arr的物体。当所有项都是相同的字母时,该节点有效。

// this function will change the letter in all the array
function fillWithZ(arr) {
    for (var i = 0; i < arr.length; ++i) {
        if (i === 4) // rare condition
            return arr; // some error here

        arr[i] = "Z";
    }

    return arr;
}

console.log(fillWithZ(["A","A","A"])) // ok, valid state
console.log(fillWithZ(["A","A","A","A","A","A"])) // bad, invalid state

如果arr成为一个不可变对象,那么我们将确保arr始终处于有效状态。

虽然其他答案都很好,但为了解决您关于实际用例的问题(来自其他答案的评论),让我们暂时离开您的运行代码,看看您眼前无处不在的答案:git。如果每次提交都会覆盖存储库中的数据,会发生什么情况?

现在我们遇到了不可变集合面临的一个问题:内存膨胀。Git非常聪明,它不会在每次更改时简单地复制文件,而是简单地跟踪差异。

虽然我不太了解git的内部工作原理,但我只能假设它使用了与您所参考的库类似的策略:结构化共享。在底层,库使用try或其他树只跟踪不同的节点。

对于内存中的数据结构,这种策略的性能也相当好,因为有一些知名的树操作算法在对数时间内运行。

另一个用例:假设你想在你的webapp上有一个撤销按钮。对于数据的不可变表示,实现这一点相对简单。但是如果您依赖于突变,这意味着您必须担心缓存世界的状态并进行原子更新。

简而言之,运行时性能和学习曲线的不可变性是要付出代价的。但是任何有经验的程序员都会告诉您,调试时间比代码编写时间要长一个数量级。运行时性能受到的轻微影响可能被用户不必忍受的与状态相关的错误所抵消。

从前,线程之间的数据同步出现了一个问题。这个问题非常麻烦,有10多个解决方案。有些人试图从根本上解决这个问题。它是函数式编程诞生的地方。这就像马克思主义。我不明白Dan Abramov是怎么把这个想法卖给JS的,因为它是单线程的。他是个天才。

我可以举个小例子。在gcc中有一个属性__attribute__((pure))。编译器试图解决你的函数是否是纯的,如果你不特别声明它。你的函数可以是纯的,即使你的状态是可变的。不可变性只是保证你的函数是纯粹的100多种方法中的一种。实际上95%的函数都是纯函数。

如果你真的没有明确的理由,你就不应该使用任何限制(比如不可变性)。如果你想“撤销”某些状态,你可以创建事务。如果你想简化通信,你可以发送带有不可变数据的事件。这取决于你。

我写这条信息来自后马克思主义共和国。我相信任何思想的激进化都是错误的。

不变性的主要优点是简单。

替换对象比修改现有对象简单。

它让你专注于一个地方的正确性。而不是每一个可能发生变化的地方。

如果你的对象处于无效状态,它更容易修复,因为错误必须发生在你创建它的时候(因为它是不可变的)

不同的看法……

我的另一个答案是从非常实际的角度来解决这个问题的,我仍然喜欢它。我决定把这个作为另一个答案,而不是对那个答案的补充,因为它是一个无聊的哲学咆哮,希望它也回答了这个问题,但并不真正符合我现有的答案。

博士TL;

即使在小型项目中,不变性也很有用,但不要认为它的存在就是为你准备的。

答案要长得多

注意:为了这个回答的目的,我使用“纪律”这个词来表示为了某些好处而自我否定。

这在形式上类似于另一个问题:“我应该使用Typescript吗?”为什么类型在JavaScript中如此重要?”它也有类似的答案。考虑以下场景:

你是5000行JavaScript/CSS/HTML代码库的唯一作者和维护者。你半技术性的老板读了一些关于typescript的最新热点,并建议我们可能想要改用它,但把决定留给你。所以你读到它,玩它,等等。

现在你要做一个选择,你是转到Typescript?

Typescript has some compelling advantages: intellisense, catching errors early, specifying your APIs upfront, ease of fixing things when refactoring breaks them, fewer tests. Typescript also has some costs: certain very natural and correct JavaScript idioms can be tricky to model in it's not-especially-powerful type system, annotations grow the LoC, time and effort of rewriting existing codebase, extra step in the build pipeline, etc. More fundamentally, it carves out a subset of possible correct JavaScript programs in exchange for the promise that your code is more likely to be correct. It's arbitrarily restrictive. That's the whole point: you impose some discipline that limits you (hopefully from shooting yourself in the foot).

回到上一段所提到的问题:值得这样做吗?

在上述场景中,我认为,如果您非常熟悉中小型JS代码库,那么选择使用Typescript更具有美感而非实用性。这很好,美学并没有错,只是它们不一定引人注目。

场景2:

你换了工作,现在是Foo公司的一名业务线程序员。你和一个10人团队一起工作,开发90000个LoC(还在不断增加)JavaScript/HTML/CSS代码库,以及一个相当复杂的构建管道,包括babel、webpack、一套polyfills、react与各种插件、一个状态管理系统、大约20个第三方库、大约10个内部库、编辑器插件(如带有内部风格指南规则的linter)等等。

Back when you were 5k LoC guy/girl, it just didn't matter that much. Even documentation wasn't that big a deal, even coming back to a particular portion of the code after 6 months you could figure it out easily enough. But now discipline isn't just nice but necessary. That discipline may not involve Typescript, but will likely involve some form of static analysis as well as all the other forms of coding discipline (documentation, style guide, build scripts, regression testing, CI). Discipline is no longer a luxury, it is a necessity.

所有这些都适用于1978年的GOTO:你的C语言的小21点游戏可以使用GOTO和意大利面逻辑,选择你自己的冒险方式并不是什么大不了的事情,但随着程序变得越来越大,野心越来越大,那么,无序地使用GOTO就不能持续下去了。所有这些都适用于今天的不变性。

就像静态类型一样,如果你不是在一个由工程师团队维护/扩展的大型代码库上工作,那么使用不可变性的选择更多的是美观而不是实用:它的好处仍然存在,但可能还没有超过成本。

但就像所有有用的学科一样,到了一定程度,它就不再是可有可无的了。如果我想保持健康的体重,那么关于冰淇淋的纪律可能是可有可无的。但如果我想成为一名有竞争力的运动员,我对是否吃冰淇淋的选择就包含在我对目标的选择之中。如果你想用软件改变世界,不可变性可能是你避免它在自身重压下崩溃所需要的一部分。