可变与不可变对象

我试图让我的头可变与不可变的对象。 使用可变对象会导致很多不好的按压(例如从方法中返回一个字符串数组),但是我很难理解这会带来什么负面影响。 什么是使用可变对象的最佳做法? 你应该尽可能避免它们吗?

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

 Map<Person, String> map = ... Person p = new Person(); map.put(p, "Hey, there!"); p.setName("Daniel"); map.get(p); // => null 

Person实例在作为关键字使用时会在地图中“丢失”,因为它的hashCodehashCode是基于可变值的。 那些值在地图之外改变,所有的哈希变得过时了。 理论家们喜欢这个问题,但实际上我并没有发现这个问题太多。

另一个方面是你的代码的逻辑“合理性”。 这是一个难以定义的术语,包含从可读性到流程性的所有内容。 一般来说,你应该能够看一段代码,并且很容易理解它的功能。 但是比这更重要的是,你应该能够说服自己,它做的是正确的 。 当对象可以在不同的代码“域”中独立地改变时,有时很难跟踪什么地方和为什么(“遥远的幽灵行动”)。 这是一个比较难以理解的概念,但是在更大,更复杂的体系结构中往往会遇到这种情况。

最后,可变对象在并发情况下是杀手级的 。 每当你从不同的线程访问一个可变对象时,你必须处理锁定。 这会降低吞吐量,使您的代码难以维护。 一个足够复杂的系统将这个问题抛到了极致,几乎不可能维持(甚至对于并发专家)。

不可变对象(更具体地说,不可变集合)避免了所有这些问题。 一旦你了解了它们的工作方式,你的代码就会发展成更容易阅读,更容易维护,不太可能以奇怪和不可预知的方式失败的东西。 不可变的对象更容易测试,这不仅因为它们易于实现,而且还涉及到它们强制执行的代码模式。 总之,他们都是好的练习!

就这样说,我不是这个问题的狂热分子。 一切都是不可变的,有些问题不能很好地模拟。 但是我确实认为你应该尽可能地向这个方向推送你的代码,当然假设你正在使用一种使之成为可信的语言的语言(C / C ++使得这非常困难,就像Java一样) 。 简而言之:优势取决于你的问题,但是我倾向于选择不变性。

不可变对象与不可变集合

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

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

当我在一个可变集合上执行一个操作时,我改变了这个集合,所有引用这个集合的实体都会看到这个变化。

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

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

不可变与可变变量/引用

一些功能性语言将不变性的概念引入自身的对象引用,只允许一个引用赋值。

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

易于开发与性能

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

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

性能问题在许多应用程序中都没有涉及,但并非全部,因此许多大型数值包(例如Python中的Numpy Array类)允许大数组的In-Place更新。 这对于使用大型矩阵和向量操作的应用领域将是非常重要的。 这些大型的数据并行和计算密集型的问题通过适当的操作实现了很大的加速。

不可变对象是一个非常强大的概念。 他们拿走了很多努力保持对象/变量对所有客户端一致的负担。

您可以将它们用于低级别的非多态对象 – 像一个CPoint类 – 主要用于值语义。

或者,您可以将它们用于高级多态接口 – 就像一个表示数学函数的IFunction一样,它只用于对象语义。

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

缺点:当与包含大量数据的对象一起使用时,内存消耗可能成为一个问题。 解决这个问题的方法可能是将操作保留在一个象征性的对象上,并做一些懒惰的评估。 然而,如果界面的设计不符合象征性的操作,这可能导致象征性的计算链,这可能会对性能产生负面影响。 在这种情况下,一定要避免从方法中返回大量的内存。 与链式符号操作相结合,这可能导致大量内存消耗和性能下降。

所以不可变的对象绝对是我关于面向对象设计思考的主要方式,但它们不是教条。 他们为客户的客体解决了很多问题,而且创造了很多,特别是对于实施者。

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

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

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

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

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

如果一个类的类型是可变的,那么这个类的变量可以有多种不同的含义。 例如,假设一个对象foo有一个int[] arr字段,它保存了一个int[3]的引用,保存着数字{5,7,9}。 即使字段的类型是已知的,但它至少可以表示四种不同的东西:

  • 一个潜在的共享引用,它的所有持有者只关心它封装值5,7和9.如果foo想要arr封装不同的值,它必须用包含所需值的不同数组替换它。 如果想要制作foo的副本,可以给副本一个arr的引用或一个保存值{1,2,3}的新数组,取其中一个更方便。

  • 在宇宙中的任何地方唯一的参考是一个数组,它封装了三个存储位置的数值5,7,9。 如果foo想要封装值5,8和9,则可以更改该数组中的第二个项目,或者创建一个包含值5,8和9的新数组,并放弃旧数组。 请注意,如果想要创建foo的副本,则必须在副本中将arr替换为新数组的引用,以使foo.arr保持为该数组中对宇宙中任何位置的唯一引用。

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

  • foo是唯一所有者的数组的引用,但由于某种原因引用由其他对象持有的引用(例如,它希望让另一个对象在那里存储数据 – 前一个案例的反面)。 在这种情况下, arr封装了数组及其内容的标识。 用一个新数组的引用来替换arr会完全改变它的含义,但是把一个clone的arr引用到foo.arr会违反foo是唯一所有者的假设。 因此没有办法复制foo

理论上, int[]应该是一个很好的简单定义好的类型,但它有四个不同的含义。 相比之下,对不可变对象(例如String )的引用通常只有一个含义。 不可变对象的“权力”大部分来自于这个事实。

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

不可变的手段不能改变,可变意味着你可以改变。

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

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

许多人认为原始数据和对象变量的最终修饰符是不可改变的,但是这并不完全正确。 所以最后几乎不意味着不可变的变量。 看这里的例子
http://www.siteconsortium.com/h/D0000F.php

可变实例通过引用传递。

不可变实例按值传递。

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

  1. 创建一个txtfile的快捷方式,并将快捷方式传递给你,或者
  2. txtfile复制副本给你。

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

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