Mutable与immutable对象
Mutable与immutable对象
我正在努力理解可变对象和不可变对象。使用可变对象会经常遇到负面评价(例如从方法中返回一个字符串数组),但我很难理解这样做的负面影响是什么。使用可变对象的最佳实践是什么?在可能的情况下应该避免使用它们吗?
不可变对象 vs. 不可变集合
在可变和不可变对象之争中的一个细节是将不可变性的概念扩展到集合。不可变对象通常表示单个逻辑数据结构(例如不可变的字符串)。当你引用一个不可变对象时,对象的内容将不会更改。
不可变集合是永远不会改变的集合。
当我在可变集合上执行操作时,我会直接改变集合,所有引用集合的实体都将看到变化。
当我在不可变集合上执行操作时,返回对反映更改的新集合的引用。所有引用以前版本集合的实体将不会看到更改。
聪明的实现不一定需要复制(克隆)整个集合以提供不可变性。最简单的例子是使用单向链表实现的堆栈和推入/弹出操作。你可以在新集合中重复使用所有来自先前集合的节点,仅为推入添加单个节点,并且不进行弹出的克隆。另一方面,对于单向链表上的push_tail操作就不是那么简单或高效了。
不可变 vs. 可变变量/引用
一些函数式语言将不可变性的概念应用于对象引用本身,只允许单个引用分配。
- 在Erlang中,对所有“变量”都是如此。我只能将对象分配给一个引用一次。如果我对集合进行操作,我将无法将新集合重新分配给旧引用(变量名)。
- Scala也在语言中构建了这一点,所有引用都使用var或val声明,val只进行单个赋值并促进一种函数式风格,而vars允许更像C或Java程序结构的结构。
- var/val声明是必需的,而许多传统语言使用可选修饰符,例如Java中的final和C中的const。
开发便捷性 vs. 性能
几乎总是使用不可变对象的原因是促进无副作用的编程和对代码的简单推理(尤其是在高度并发/并行环境中)。如果对象是不可变的,你不必担心另一个实体改变基础数据。
主要缺点是性能。这是我在Java中进行了一个简单测试的写作,比较了一些toy problem中的不可变和可变对象。
这些性能问题在许多应用程序中都可以忽略不计,但并不是所有应用程序,这就是为什么许多大型数值包,例如Python中的Numpy数组类,允许对大型数组进行就地更新。这对于利用大型矩阵和向量操作的应用领域非常重要。这些大数据并行和计算密集型问题可以通过就地操作实现巨大的加速。
这个问题有几个方面需要考虑。
-
没有引用识别的可变对象可能会在奇怪的时候导致bug。比如,考虑一个具有基于值的
equals
方法的Person
bean:Map
map = ... Person p = new Person(); map.put(p, "Hey, there!"); p.setName("Daniel"); map.get(p); // => null 当将
Person
实例用作键时,它在 map 中被“丢失”,因为其hashCode
和相等性基于可变值。这些值在 map 外部改变了,所有的哈希值都变得过时了。理论家喜欢琢磨这一点,但实践中,我没有发现它是太大的问题。 -
另一个方面是代码的逻辑“合理性”。这是一个难以定义的术语,包括可读性和流程等方面。一般来说,你应该能够查看代码并轻松理解它所做的事情。但比这更重要的是,你应该能够让自己相信它的正确性。当对象可以在不同的代码“域”中独立地发生变化时,有时会难以跟踪是什么、在哪里以及为什么会这样(“鬼魂般的远程操作”)。这是一个更难以示范的概念,但在更大、更复杂的架构中经常遇到。
-
最后,在并发情况下,可变对象是致命的。当你从不同的线程中访问可变对象时,你必须处理锁定。这会降低吞吐量,并使你的代码难以维护。一个足够复杂的系统会把这个问题放大到几乎不可能维护的程度(即使对于并发专家也是如此)。
不可变对象(尤其是不可变集合)避免了所有这些问题。一旦你理解了它们的工作方式,你的代码会变得更容易阅读、更容易维护,而且不太可能以奇怪和不可预测的方式失败。不可变对象更容易测试,不仅因为它们易于模拟,而且因为它们倾向于实施的代码模式。总之,不可变对象在各个方面都是好的实践!
话虽如此,我并不是一个狂热者。有些问题在所有内容都是不可变的情况下并不适合建模。但我认为你应该尽可能地将你的代码推向这个方向,当然,前提是你使用的语言使这成为一种可行的观点(C/C++ 使这非常困难,Java 也是如此)。简而言之:优势在某种程度上取决于你的问题,但我倾向于选择不可变性。