我试图让我的头围绕可变对象和不可变对象.使用可变对象会导致很多不良操作(例如,从方法中返回一个字符串数组),但我无法理解其中的负面影响.使用可变对象的最佳做法是什么?你应该尽可能地避开它们吗?
嗯,这有几个方面.第一,没有引用标识的可变对象可能会在奇数时间导致错误.例如,考虑Person
使用基于值的equals
方法的bean :
Mapmap = ... Person p = new Person(); map.put(p, "Hey, there!"); p.setName("Daniel"); map.get(p); // => null
Person
当用作键时,实例在映射中"丢失",因为它的hashCode
相等性基于可变值.这些值在地图外部发生了变化,所有散列都已过时.理论家们喜欢在这一点上竖起来,但在实践中我并没有发现这是一个太大的问题.
另一个方面是代码的逻辑"合理性".这是一个很难定义的术语,包括从可读性到流动性的所有内容.通常,您应该能够查看一段代码并轻松了解它的作用.但更重要的是,你应该能够说服自己,它能够正确地做到这一点.当对象可以在不同的代码"域"之间独立地改变时,有时难以跟踪什么是在哪里以及为什么("远处的怪异动作").这是一个更难以举例说明的概念,但它常常面向更大,更复杂的架构.
最后,可变对象在并发情况下是杀手锏.无论何时从单独的线程访问可变对象,都必须处理锁定.这减少了产量,使你的代码大大更难维护.一个足够复杂的系统将此问题远远超出了比例,几乎无法维护(即使对于并发专家).
不可变对象(更具体地说,不可变集合)避免了所有这些问题.一旦你开始思考它们是如何工作的,你的代码就会变得更容易阅读,更容易维护,并且不太可能以奇怪和不可预测的方式失败.不可变对象甚至更容易测试,因为它们不仅易于模拟,而且还有他们倾向于强制执行的代码模式.简而言之,它们都是很好的练习!
话虽如此,我在这件事上几乎不是狂热者.当一切都是不可变的时,有些问题就不能很好地建模.但我确实认为你应该尝试尽可能多地将代码推向这个方向,当然假设你正在使用一种语言来使这成为一个站得住脚的意见(C/C++使得这非常困难,Java也是如此) .简而言之:优势在某种程度上取决于你的问题,但我倾向于选择不变性.
关于可变对象和不可变对象的争论中的一个更好的观点是将不变性概念扩展到集合的可能性.不可变对象是一个通常表示数据的单个逻辑结构的对象(例如,不可变的字符串).当您引用不可变对象时,该对象的内容不会更改.
不可变集合是一个永不改变的集合.
当我对可变集合执行操作时,我会更改集合,并且所有引用该集合的实体都将看到更改.
当我对不可变集合执行操作时,引用将返回到反映更改的新集合.引用先前版本集合的所有实体都不会看到更改.
聪明的实现不一定需要复制(克隆)整个集合以提供不变性.最简单的例子是实现为单链表和推/弹操作的堆栈.您可以重新使用新集合中前一个集合中的所有节点,仅为该集合添加单个节点,并且不为该集合克隆任何节点.另一方面,单链表上的push_tail操作不是那么简单或有效.
一些函数式语言将不变性的概念应用于对象引用本身,只允许单个引用赋值.
在Erlang中,所有"变量"都是如此.我只能将对象分配给引用一次.如果我要对集合进行操作,我将无法将新集合重新分配给旧引用(变量名称).
Scala还将其构建为语言,所有引用都使用var或val声明,vals只是单一赋值和提升函数样式,但vars允许更像c或类似java的程序结构.
var/val声明是必需的,而许多传统语言使用可选的修饰符,例如java中的final和c中的const.
几乎总是使用不可变对象的原因是促进副作用自由编程和关于代码的简单推理(特别是在高度并发/并行环境中).如果对象是不可变的,则不必担心另一个实体更改的基础数据.
主要缺点是性能.这是一篇关于我在Java中做的简单测试的文章,比较玩具问题中的一些不可变对象和可变对象.
性能问题在许多应用程序中都没有问题,但并非全部,这就是为什么许多大型数字包(例如Python中的Numpy Array类)允许大型数组的就地更新.这对于使用大型矩阵和矢量运算的应用领域非常重要.这些大数据并行和计算密集型问题通过适当的操作实现了极大的加速.
查看此博客文章:http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html.它解释了为什么不可变对象比可变对象更好.简而言之:
不可变对象的构造,测试和使用都比较简单
真正不可变的对象始终是线程安全的
它们有助于避免时间耦合
他们的使用是无副作用(没有防御性副本)
避免了身份可变性问题
他们总是有失败的原子性
它们更容易缓存
不可变对象是一个非常强大的概念.它们消除了为所有客户保持对象/变量一致的大量负担.
您可以将它们用于低级非多态对象(如CPoint类),这些对象主要用于值语义.
或者您可以将它们用于高级多态接口 - 如表示数学函数的IFunction - 专门用于对象语义.
最大的优势:不变性+对象语义+智能指针使对象所有权成为非问题,默认情况下,对象的所有客户端都有自己的私有副本.隐含地,这也意味着存在并发性时的确定性行为.
缺点:当与包含大量数据的对象一起使用时,内存消耗可能成为一个问题.对此的解决方案可能是将对象的操作保持为符号,并进行惰性求值.然而,如果接口不是为了容纳符号操作,那么这可能导致符号计算链,这可能会对性能产生负面影响.在这种情况下肯定要避免的是从方法中返回大量内存.与链式符号操作相结合,这可能导致大量内存消耗和性能下降.
因此,不可变对象绝对是我思考面向对象设计的主要方式,但它们不是教条.它们为对象的客户端解决了很多问题,但也创建了许多问题,特别是对于实现者.
您应该指定您正在谈论的语言.对于像C或C++这样的低级语言,我更喜欢使用可变对象来节省空间并减少内存流失.在高级语言中,不可变对象使得更容易推断代码的行为(特别是多线程代码),因为没有"远距离的怪异行为".