不可变集合?

26

我正在让应用程序中的大多数基本类型都是不可变的。但集合类型也应该是不可变的吗?对我来说,这似乎是一个巨大的开销,除非我有所遗漏。

我说的是用于保存 Point3 值等的集合,可以在不同时间添加。所以如果集合中有 1 百万个值,并且您需要删除其中一个值,那么您将不得不重新创建相同的集合,对吧?


6
对于不可变集合中不变部分的重复利用有一些方法。如果您在两端添加元素,堆栈和列表通常可以100%地重复使用;而对于树,则通常只需要重新构建O(lg n)个节点即可。请参阅我的博客上的“不可变性”归档,了解更多相关想法。 - Eric Lippert
11
但话说回来:选择能够很好地模拟您问题的工具。如果您的问题可以通过可变集合进行良好建模,则使用可变集合。换句话说,如果集合在发生变化后仍保留其身份,则使其可变。相反地,如果您认为集合在变化时是完全不同的集合,则使用不可变集合更加合适。 - Eric Lippert
10个回答

36

2
链接已经失效。可以在这里找到系列中每个条目的有效链接:http://weblogs.asp.net/bleroy/immutability-in-c。 - user2609980
链接似乎有效 - 它们都指向微软文档,因此应该非常稳定。 - Sudhanshu Mishra
@SudhanshuMishra 当然可以工作,因为这篇文章是在2020年编辑的。 - Piotr Golacki

16

不可变集合非常好,特别是如果您的应用程序已经利用了不可变类型或语义。

.NET刚刚发布了他们的第一个不可变集合,我建议您尝试一下。


3
很遗憾,它只适用于4.5版本及以上。 - Kugel
这些听起来不错。我更喜欢使用方法名,清楚地表明它们不会修改基础集合(例如看到 myList.WidthAddedItem("George"); 的人更可能认识到正在创建和丢弃一个新的修改后的列表,而不是看到 myList.Add("George") 的人),但除此之外它们听起来非常好。顺便说一下,关于文章的一个问题:ReadOnlyCollection<T> 提供了一个接口也可以保证(对于正确的实现)但所提到的接口没有的承诺:引用不能用于修改集合。 - supercat
此外,在您的性能图表中,您没有提到通过索引访问列表项的性能。如果我按照所描述的实现一个ImmutableList类型,索引操作通常会是O(lgN),但在某些特定的常见情况下,例如直接从数组生成的不可变列表,则为O(1)。那么您实际实现的与之相比如何? - supercat
1
啊等等,实际上不行,那个延迟优化是行不通的——因为在此期间,数组可能会被持有对原始数组的引用的其他人更改,这将违反不可变保证。因此,我们确实需要立即转换数据结构,这意味着每次查找都需要O(log n)的时间。 - Andrew Arnott
@AndrewArnott:从数组初始化结构需要复制数组,但是通过直接线性复制数组就不必排除使用该数组的段以适应修改。如果“George”是从一个1500个元素的数组中加载的实例,而我们需要一个新的实例“Fred”,其中第23项不同,可以创建一个“Fred”,它说实际上“使用George的0-22项目,然后是单个更新的项目,然后是George的24-149项”。要使用的最优算法取决于使用模式,但我认为使用数组... - supercat
显示剩余6条评论

9

我最喜欢使用集合的技巧就是从不在不同对象之间传递它们。如果它们只存在于一个对象内部,那么使它们不可变基本上是无关紧要的(只要包含它们的对象不改变它们,它们就不会改变)。

通常你的集合代表着某些东西,对吧?它是一组狗或一组发票...

通常有一些你可以对一组狗做的事情(放牧?绝育?),或者一组发票可以做的事情(付款?)。几乎总会有一些操作应用于整个对象列表--这些操作具有超出单个发票.pay()功能的功能(例如,确保最重要的发票首先得到支付),如果没有一个包装器来包装你的集合,那么真的没有地方可以放置这些操作。

同时,通常将几个变量与您的集合相关联也是有意义的--再次,如果没有包装器,您总是会将这些变量放在一些奇怪的不自然的位置。

起初可能会感觉很奇怪,但在评判之前请尝试几次。


2

如果您只是从开头或结尾添加/删除,您可能可以作弊 - 但通常情况下,是的:这意味着您需要为每个更改创建一个新的集合。

所以:您需要有效地更改集合吗?如果是这样,并且考虑到它们的大小:我会倾向于查看同步访问(而不是使它们完全不可变)。查看lock(又名Monitor)。


2
我同意Eric关于选择解决问题的正确工具的评论。当你的目标包括提供清晰的身份语义或使实现在并行计算环境中更易于使用时,不可变性会增加价值。不可变性也可以通过允许缓存或透明代理等优化来帮助提高性能。
但是,不可变性也可能会带来性能成本,特别是当你使用“写时复制”模式来模拟“更改”时。
你必须决定为什么要使你的实体/集合不可变-这将有助于决定是否这样做。

1
你可以将公共接口定义为IEnumerable,但在实现中仍然使用可变集合。

0
一个查找表会成为一个不错的不可变集合。它不需要改变大小,你希望它是静态的,这样在查找棘手的计算时就可以快速查找。如果以后需要添加一些东西,那么我不会费心去保持不可变性,因为这违背了初衷。

0

这取决于你的程序编写/设计的风格。

当你使用函数式编程风格时,不可变集合才有意义(命令式设计的程序不应该使用它们)。

就像在函数式语言中一样,你应该使用链表,可以每个元素以O(1)的时间复杂度构建(cons),并从中进行函数式处理(递归,从列表构建新列表)。

当你的程序需要命令式集合(数组、向量/列表)时,请保持它们是可变的。


-1

这完全取决于同时使用集合的人。字符串是不可变的,以防止出现两个线程同时尝试删除第一个字符等错误。


-2

如果您有一个在构建后可以添加项目的集合,则它不是不可变的


4
他知道这一点,他在问它们是否应该是不可变的。 - n8wrl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接