关于oop:可变对象与不可变对象

Mutable vs immutable objects

我正试图让我的头脑围绕着易变和不变的物体。使用可变对象会收到很多坏消息(例如,从方法返回字符串数组),但我很难理解这会带来什么负面影响。使用可变对象的最佳实践是什么?你应该尽可能避免他们吗?


嗯,这有几个方面。第一,没有引用标识的可变对象可能会在奇数时间导致错误。例如,考虑使用基于值的equals方法的Personbean:

1
2
3
4
5
6
Map<Person, String> map = ...
Person p = new Person();
map.put(p,"Hey, there!");

p.setName("Daniel");
map.get(p);       // => null

当用作键时,Person实例在映射中"丢失",因为它是hashCode,并且相等性基于可变值。这些值在映射之外发生了变化,所有散列操作都变得过时了。理论家们喜欢喋喋不休地讨论这一点,但实际上我并没有发现这是一个太多的问题。

另一个方面是代码的逻辑"合理性"。这是一个难以定义的术语,涵盖了从可读性到流的所有内容。一般来说,您应该能够查看一段代码并轻松理解它的作用。但更重要的是,你应该能够说服自己,它做了正确的事情。当对象可以在不同的代码"域"中独立地更改时,有时很难跟踪什么是在哪里以及为什么("在远处的恐怖行为")。这是一个更难举例说明的概念,但在更大、更复杂的体系结构中,这是经常遇到的问题。

最后,可变对象在并发情况下是杀手。无论何时从不同的线程访问可变对象,都必须处理锁定。这会降低吞吐量,并使代码更难维护。一个足够复杂的系统将这个问题吹得远远超出了比例,以至于几乎不可能进行维护(即使对于并发专家也是如此)。

不可变对象(尤其是不可变集合)可以避免所有这些问题。一旦你开始考虑它们是如何工作的,你的代码就会发展成更容易阅读,更容易维护,并且不太可能以奇怪和不可预测的方式失败的东西。不可变对象更容易测试,这不仅是因为它们易于模仿,而且还因为它们倾向于强制执行代码模式。简而言之,它们是很好的练习!

尽管如此,我在这件事上可不是一个狂热分子。当一切都是不变的时,有些问题就不能很好地建模。但我确实认为,你应该尽可能地把代码推到那个方向,当然,假设你使用的是一种使这一观点变得可信的语言(C/C++使这非常困难,就像Java一样)。简而言之:优势在某种程度上取决于你的问题,但我倾向于不可变。


不可变对象与不可变集合

在关于可变对象和不可变对象的争论中,一个更为微妙的观点是将不可变概念扩展到集合的可能性。不可变对象是通常表示单个数据逻辑结构(例如不可变字符串)的对象。当您引用一个不可变的对象时,该对象的内容将不会改变。

不可变集合是永不更改的集合。

当我对可变集合执行操作时,我会就地更改集合,并且所有引用该集合的实体都将看到更改。

当我对不可变集合执行操作时,会将引用返回到反映更改的新集合。所有引用了集合早期版本的实体都将看不到更改。

聪明的实现不一定需要复制(克隆)整个集合以提供不可变性。最简单的例子是作为单个链表实现的堆栈和push/pop操作。您可以重用新集合中上一个集合中的所有节点,只为推送添加一个节点,并且不为POP克隆任何节点。另一方面,单链表的push-tail操作并不简单或高效。

不可变与可变变量/引用

有些函数语言采用对象引用本身不可变的概念,只允许一个引用赋值。

  • 在Erlang中,所有"变量"都是这样。我只能将对象分配给引用一次。如果要对集合进行操作,我将无法将新集合重新分配给旧引用(变量名)。
  • Scala也将此构建为语言,所有的引用都是用var或Var声明的,VALS仅是单个赋值和促进函数样式,但是VARS允许更多的类C或类Java程序结构。
  • VAL/VALL声明是必需的,而许多传统语言使用可选的修饰符,如JAVA中的JAVA和C中的CONST。

易开发性与性能

几乎所有情况下,使用不可变对象的原因都是为了促进无副作用编程和代码的简单推理(尤其是在高度并发/并行环境中)。如果对象是不可变的,则不必担心基础数据会被另一个实体更改。

主要的缺点是性能。下面是我在Java中做的一个简单的测试,比较了玩具问题中一些不可变的和可变的对象。

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


查看此博客帖子:http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html。它解释了为什么不变对象比可变对象更好。简而言之:

  • 不可变对象易于构造、测试和使用
  • 真正不变的对象总是线程安全的
  • 它们有助于避免时间耦合
  • 其使用无副作用(无防御性副本)
  • 避免了身份可变性问题
  • 他们总是有失败的原子性
  • 它们更容易缓存

不变的物体是一个非常强大的概念。它们消除了为所有客户机保持对象/变量一致性的许多负担。

您可以将它们用于低级的、非多态的对象(如cpoint类),这些对象主要用于值语义。

或者,您可以将它们用于高级的多态接口,比如表示数学函数的ifunction,它专门用于对象语义。

最大的优点:不变性+对象语义+智能指针使对象所有权成为一个无问题的问题,默认情况下,对象的所有客户端都有自己的私有副本。这也意味着存在并发性时的确定性行为。

缺点:当与包含大量数据的对象一起使用时,内存消耗会成为一个问题。解决这个问题的方法可能是保持对象上的操作是符号化的,并进行懒惰的计算。但是,如果接口不是为适应符号操作而设计的,那么这可能会导致符号计算链,从而对性能产生负面影响。在这种情况下,一定要避免的是从一个方法返回大量的内存。结合链式符号操作,这可能导致大量的内存消耗和性能下降。

所以不变的对象绝对是我考虑面向对象设计的主要方式,但它们不是教条。它们为对象的客户机解决了许多问题,但也创建了许多问题,特别是对于实现者。


你应该指定你所说的语言。对于像C或C++这样的低级语言,我更喜欢使用可变对象来节省空间和减少内存流失。在更高级的语言中,不可变的对象更容易解释代码的行为(尤其是多线程代码),因为在远处没有"可怕的动作"。


可变对象只是在创建/实例化后可以修改的对象,而不可变对象则不能修改(请参见主题的维基百科页面)。编程语言中的一个例子是pythons列表和元组。列表可以修改(例如,创建后可以添加新项),而元组不能。

我真的不认为有一个明确的答案,即哪一个更适合所有情况。他们都有自己的位置。


如果类类型是可变的,那么该类类型的变量可以有许多不同的含义。例如,假设一个对象foo有一个字段int[] arr,它引用了一个包含数字5、7、9的int[3]。尽管字段的类型已知,但它至少可以表示四种不同的内容:

  • 一个潜在的共享引用,其所有持有者只关心它封装了值5、7和9。如果foo希望arr封装不同的值,则必须用包含所需值的不同数组替换它。如果要复制foo,可以引用arr,或者使用一个新数组保存值1、2、3,以更方便的为准。

  • 在宇宙的任何地方,唯一一个对数组的引用,该数组封装了值5、7和9。当前保存值5、7和9的三个存储位置的集合;如果foo希望它封装值5、8和9,它可以更改该数组中的第二个项,或者创建一个保存值5、8和9的新数组,并放弃旧数组。请注意,如果要复制foo,则必须在副本中使用新数组的引用替换arr,以便foo.arr保留为宇宙中任何位置对该数组的唯一引用。

  • 对某个数组的引用,该数组由某个其他对象拥有,该对象出于某种原因(例如,它可能希望foo在其中存储一些数据)将其暴露给foo。在这种情况下,arr不封装数组的内容,而是封装数组的标识。因为用对新数组的引用替换arr将完全改变其含义,所以foo的副本应保存对同一数组的引用。

  • 对一个数组的引用,其中foo是该数组的唯一所有者,但其他对象出于某种原因持有该数组的引用(例如,它希望在该数组中存储另一个对象,即前一个实例的反面)。在这个场景中,arr封装了数组的标识及其内容。将arr替换为对新数组的引用将完全改变其含义,但如果克隆人的arr引用foo.arr,将违反foo是唯一所有者的假设。因此无法复制foo

从理论上讲,int[]应该是一个很好的简单定义明确的类型,但它有四个非常不同的含义。相比之下,对不可变对象(如String)的引用通常只有一个含义。许多不变物体的"力量"都源于这个事实。


可变实例通过引用传递。

不可变实例按值传递。

抽象示例。假设在我的HDD中存在一个名为txtfile的文件。现在,当您向我询问txtfile时,我可以以两种模式返回它:

  • 创建指向txtfile和pas的快捷方式,或者
  • 为txtfile和pas复制一份副本。
  • 在第一种模式下,返回的txtfile是可变文件,因为在快捷方式文件中进行更改时,也会在原始文件中进行更改。这种模式的优点是,每个返回的快捷方式所需的内存(在RAM或HDD中)都更少,缺点是每个人(不仅是我,所有者)都有修改文件内容的权限。

    在第二种模式下,返回的txtfile是不可变的文件,因为接收到的文件中的所有更改都不引用原始文件。这种模式的优点是只有我(所有者)可以修改原始文件,缺点是每个返回的副本都需要内存(在RAM或HDD中)。


    不变的意思是不能改变的,可变的意思是你可以改变。

    对象与Java中的基元不同。基本体是内置类型(布尔型、int型等),对象(类)是用户创建的类型。

    当在类的实现中定义为成员变量时,基元和对象可以是可变的或不可变的。

    很多人认为原语和对象变量在它们前面有一个最后的修饰语是不可变的,然而,这并不完全正确。所以,对于变量来说,final几乎不意味着不可变。请参阅此处的示例http://www.siteconsumptium.com/h/d0000f.php。


    如果返回数组或字符串的引用,则外部世界可以修改该对象中的内容,从而使其成为可变(可修改)对象。