可变对象与不可变对象

时间:2008-10-18 07:28:55

标签: oop immutability mutable

我正试图绕过可变对象和不可变对象。使用可变对象会导致很多不良操作(例如,从方法中返回一个字符串数组),但我无法理解其中的负面影响。使用可变对象的最佳做法是什么?你应该尽可能地避开它们吗?

12 个答案:

答案 0 :(得分:153)

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

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也是如此) 。简而言之:优势在某种程度上取决于你的问题,但我倾向于选择不变性。

答案 1 :(得分:25)

不可变对象与不可变集合

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

不可变集合是一个永不改变的集合。

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

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

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

不可变与可变变量/参考

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

  • 在Erlang中,所有“变量”都是如此。我只能将对象分配给引用一次。如果我要对集合进行操作,我将无法将新集合重新分配给旧引用(变量名称)。
  • Scala还将此构建为语言,所有引用都声明为 var val ,vals仅为单一赋值并提升功能样式,但vars允许更多类似c或java的程序结构。
  • var / val声明是必需的,而许多传统语言使用可选的修饰符,例如java中的 final 和c中的 const

易于开发与性能

使用不可变对象的原因几乎总是促进副作用自由编程和关于代码的简单推理(特别是在高度并发/并行环境中)。如果对象是不可变的,则不必担心另一个实体更改的基础数据。

主要缺点是性能。这是一篇关于a simple test I did in Java的一篇文章,比较玩具问题中的一些不可变对象和可变对象。

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

答案 2 :(得分:10)

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

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

或者你可以将它们用于高级多态接口 - 比如代表数学函数的IFunction - 专门用于对象语义。

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

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

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

答案 3 :(得分:10)

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

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

答案 4 :(得分:6)

您应该指定您正在谈论的语言。对于像C或C ++这样的低级语言,我更喜欢使用可变对象来节省空间并减少内存流失。在更高级别的语言中,不可变对象使得更容易推断代码的行为(特别是多线程代码),因为没有“远距离的怪异行为”。

答案 5 :(得分:4)

一个可变对象只是一个可以在创建/实例化之后修改的对象,而不是一个无法修改的不可变对象(请参阅主题上的the Wikipedia page)。编程语言中的一个例子是Pythons列表和元组。可以修改列表(例如,可以在创建后添加新项目),而元组则不能。

我真的不认为对于所有情况哪一个更好的答案是明确的。他们都有自己的位置。

答案 6 :(得分:1)

如果类类型是可变的,则该类类型的变量可以具有许多不同的含义。例如,假设一个对象foo有一个字段int[] arr,并且它包含一个对int[3]持有数字{5,7,9}的引用。即使已知该字段的类型,它至少可以表示四种不同的东西:

  • 潜在共享引用,所有持有者只关心它封装了值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)通常只有一个含义。不可变对象的大部分“力量”源于这一事实。

答案 7 :(得分:1)

通常,将

可变集合用于其不可变集合时, 操作。

但是,可变性是有代价的:您需要更加谨慎地在它们之间共享它们 程序的不同部分。

在共享的可变集合更新时,很容易创建错误 出乎意料的是,强迫您在大型代码库中的哪一行执行不必要的更新。

一种常见的方法是在一个函数内部局部使用 mutable 集合,或者在存在该函数的类私有 是性能瓶颈,但要在其他不太关心速度的地方使用 immutable 集合。

这在最重要的地方为您提供了 mutable 集合的高性能,同时又不牺牲 不可变集合为您在整个应用程序逻辑中提供的安全性。

答案 8 :(得分:0)

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

答案 9 :(得分:0)

不可改变的手段无法改变,可变的手段可以改变。

对象与Java中的基元不同。基元以类型(boolean,int等)构建,对象(类)是用户创建的类型。

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

许多人认为原型和对象变量在其前面具有最终修饰符是不可变的,但是,这并不完全正确。所以最终几乎并不意味着变量是不可变的。见这里的例子
http://www.siteconsortium.com/h/D0000F.php

答案 10 :(得分:0)

Mutable 实例通过引用传递。

不可变的 实例按值传递。

抽象例子。假设我的硬盘中存在名为 txtfile 的文件。现在,当你向我询问 txtfile 时,我可以用两种模式返回它:

  1. 创建 txtfile 的快捷方式并为您提供快捷方式,或
  2. 获取 txtfile 的副本并将副本发送给您。
  3. 在第一种模式下,返回的 txtfile 是一个可变文件,因为当您在快捷方式文件中进行更改时,您也会对原始文​​件进行更改。这种模式的优点是每个返回的快捷方式都需要更少的内存(在RAM或HDD中),缺点是每个人(不仅是我,所有者)都有权修改文件内容。

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

答案 11 :(得分:0)

Unmodifiable-是可修改的包装。保证不能直接更改(但可能使用后备对象)

Immutable-创建后无法更改其状态。当其所有字段都是不可变的时,对象是不可变的。这是不可修改对象的下一步

线程安全

不可变对象的主要优点在于,它自然适用于并发环境。并发性的最大问题是shared resource,可以在任何线程中更改它。但是,如果对象是不可变的,则read-only是线程安全的操作。对原始不可变对象的任何修改都会返回一个副本

真理之源,没有副作用

作为开发人员,您完全可以确定不能从任何地方更改不可变对象的状态(有意或无意)。例如,如果消费者使用不可变对象,那么他可以使用原始不可变对象

编译优化

提高性能

缺点:

与更改可变对象相比,复制对象的操作更为繁琐,这就是为什么它具有一些性能足迹的原因

要创建immutable对象,请使用:

1。语言级别

每种语言都包含可以帮助您的工具。例如:

  • Java具有finalprimitives
  • Swift有letstruct [About]

语言定义变量的类型。例如:

  • Java具有primitivereference类型,
  • Swift具有valuereference类型[About]

对于immutable对象而言,更方便的是primitivesvalue类型,它们默认情况下进行复制。至于reference类型,则比较困难(因为您可以从中更改对象的状态),但是可以。例如,您可以在开发人员级别使用clone模式来制作deep(而不是shallow)副本。

2。开发人员级别

作为开发人员,您不应该提供用于更改状态的界面

[Swift][Java]不可变集合