可变和不可变类的优缺点

11 浏览
0 Comments

可变和不可变类的优缺点

我正在努力理解可变对象与不可变对象之间的区别。使用可变对象会受到很多负面评价(例如从方法返回字符串数组),但我很难理解这样做的负面影响是什么。在使用可变对象方面有哪些最佳实践?是否应尽量避免使用可变对象?

0
0 Comments

不可变类和可变类的优缺点

在关于可变类和不可变类的辩论中,将不可变性的概念扩展到集合的可能性是其中的一个细节。不可变对象是经常表示数据的单一逻辑结构的对象(例如不可变字符串)。当你引用一个不可变对象时,对象的内容不会改变。

不可变集合是从不改变的集合。

当我对可变集合执行操作时,我会直接改变集合,并且所有引用集合的实体都会看到这个改变。

当我对不可变集合执行操作时,会返回一个引用到反映改变的新集合。所有引用之前版本集合的实体将不会看到这个改变。

聪明的实现不一定需要复制(克隆)整个集合以提供不可变性。最简单的例子是作为单链表和推入/弹出操作实现的堆栈。你可以在新集合中重用之前集合的所有节点,只需为推送添加一个单节点,并为弹出不克隆任何节点。然而,对于单链表上的push_tail操作,不是那么简单或高效。

不可变变量/引用与可变变量/引用

一些函数式语言将不可变性的概念扩展到对象引用本身,只允许一次引用赋值。

- 在Erlang中,对于所有“变量”都是如此。我只能将对象分配给一个引用一次。如果我对集合进行操作,我将无法将新集合重新分配给旧引用(变量名)。

- Scala也在语言中构建了这一特性,所有引用都使用var或val声明,val只能赋值一次,倡导函数式风格,而var允许更类似C或Java的程序结构。

- var/val声明是必需的,而许多传统语言使用可选的修饰符,如Java中的final和C中的const。

开发的简易性与性能

使用不可变对象的原因几乎总是为了促进无副作用的编程和对代码的简单推理(特别是在高并发/并行环境中)。如果对象是不可变的,你就不必担心底层数据被另一个实体改变。

主要的缺点是性能。这里有一篇关于在Java中进行的一个简单测试的文章,比较了一些不可变对象和可变对象在一个玩具问题中的性能。

在许多应用程序中,性能问题并不重要,但并不是全部,这就是为什么许多大型数值包(如Python中的Numpy数组类)允许对大型数组进行就地更新的原因。这对于使用大型矩阵和向量操作的应用领域非常重要。这些大型的数据并行和计算密集型问题通过就地操作实现了巨大的加速。

0
0 Comments

可变类和不可变类的优缺点是什么?

可变对象在没有引用标识的情况下可能会在奇怪的时候引发错误。例如,考虑一个具有基于值的equals方法的Person bean:

Map map = ...

Person p = new Person();

map.put(p, "Hey, there!");

p.setName("Daniel");

map.get(p); // => null

当作为键使用时,Person实例在地图中“丢失”,因为其hashCode和相等性基于可变值。这些值在地图外部发生了变化,所有的哈希计算都变得过时。理论家们喜欢强调这一点,但实践中我发现这不是太大的问题。

另一个方面是代码的逻辑“合理性”。这是一个难以定义的术语,涵盖了从可读性到流程的一切。一般来说,你应该能够查看一段代码并轻松理解它的功能。但比这更重要的是,你应该能够说服自己它以正确的方式完成了它的功能。当对象可以在不同的代码“域”中独立更改时,有时很难跟踪它们的位置和原因(“远程作用”)。这是一个更难以说明的概念,但在更大、更复杂的架构中经常面临这个问题。

最后,可变对象在并发情况下是致命的。每当你从不同的线程访问可变对象时,你都必须处理锁定。这会降低吞吐量,并使你的代码难以维护。一个足够复杂的系统会夸大这个问题,使其几乎无法维护(即使对于并发专家也是如此)。

不可变对象(尤其是不可变集合)避免了所有这些问题。一旦你理解了它们的工作原理,你的代码将变得更容易阅读、更容易维护,而且不太可能出现奇怪和不可预测的错误。不可变对象甚至更容易进行测试,不仅因为它们易于模拟,还因为它们强制执行的代码模式。简而言之,它们是全方位的良好实践!

话虽如此,我在这个问题上并不是一个狂热者。有些问题在一切都是不可变的情况下无法很好地建模。但我认为你应该尽可能地将你的代码朝着这个方向推进,当然前提是你使用的语言使得这成为一个可行的观点(C/C++使这变得非常困难,Java也是如此)。简而言之:优点在一定程度上取决于你的问题,但我倾向于更喜欢不可变性。

很好的回答。然而,有一个小问题:C++对不可变性有很好的支持吗?const-correctness特性不足以解决吗?

C++实际上具有一些更基本的特性,尤其是更好的区分用于封装非共享状态的存储位置和封装对象标识的存储位置。

在Java编程中,Joshua Bloch在他著名的书《Effective Java (Item 15)》中对这个主题进行了很好的解释。

0