我正在尝试了解可变对象与不可变对象。使用可变对象会受到很多负面报道(例如从方法返回字符串数组),但我无法理解这样做的负面影响。使用可变对象的最佳实践是什么?您应该尽可能避免它们吗?

有帮助吗?

解决方案

嗯,这有几个方面。第一,没有引用标识的可变对象可能会在奇数时间导致错误。例如,考虑使用基于值的 equals 方法的 Person 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也是如此) 。简而言之:优势在某种程度上取决于你的问题,但我倾向于选择不变性。

其他提示

不可变对象 vs 不可变对象不可变集合

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

不可变集合是永远不会改变的集合。

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

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

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

不变 vs 不变可变变量/引用

一些函数式语言采用对象引用本身的不变性概念,只允许单个引用分配。

  • 在 Erlang 中,所有“变量”都是如此。我只能将对象分配给引用一次。如果我要对集合进行操作,我将无法将新集合重新分配给旧引用(变量名称)。
  • Scala 还将其构建到语言中,所有引用都用 变量 或者 瓦尔, vals 仅是单一赋值并促进函数式风格,但 vars 允许更像 c 或 java 的程序结构。
  • var/val 声明是必需的,而许多传统语言使用可选修饰符,例如 最终的 在java中和 常量 在c。

开发容易度对比表现

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

主要缺点是性能。这是一篇关于 我用Java做了一个简单的测试 比较一些不可变的与不可变的玩具问题中的可变对象。

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

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

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

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

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

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

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

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

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

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

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

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

如果类类型是可变的,则该类类型的变量可以具有多种不同的含义。例如,假设一个对象 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) 一般只有一种含义。不可变对象的大部分“力量”都源于这个事实。

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

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

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

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

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

可变的 实例通过引用传递。

不可变的 实例按值传递。

抽象的例子。假设存在一个名为 txt文件 在我的硬盘中。现在,当你问 txt文件 从我这里,我可以通过两种模式返回它:

  1. 创建快捷方式 txt文件 并向您传递快捷方式,或者
  2. 复印一份 txt文件 并将其复制给您。

在第一种模式下,返回 txt文件 是一个可变文件,因为当您对快捷方式文件进行更改时,您也会对原始文​​件进行更改。这种模式的优点是每个返回的快捷方式需要较少的内存(RAM 或 HDD),缺点是每个人(不仅仅是我,所有者)都有权修改文件内容。

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top