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