为什么要使用ImmutableList而不是ReadOnlyCollection?

101

.NET 4.5有一个新的命名空间System.Collections.Immutable

该包提供了线程安全且保证永远不会更改其内容的集合,也称为不可变集合。

我感到困惑。 ReadOnlyCollection类已经解决了线程安全问题,为什么要使用ImmutableList呢?


我知道还有一个IReadOnlyList 接口。这并不能隐式解决线程安全问题,因为其他线程可能会通过另一个接口编辑对象。


1
这篇文章似乎解释得相当清楚:“http://blog.slaks.net/2013-06-11/readonly-vs-immutable/”。 - Damian
2
仅供参考,System.Collections.Immutable 必须通过 NuGet 包管理器安装。我在一个针对 .Net Framework 4.6.2 的项目中不得不这样做。 - RBT
6个回答

99

使用 ReadOnlyCollection

只读集合只是一个具有包装器的集合,防止修改集合;因此,如果对基础集合进行更改,则只读集合会反映这些更改。

这在 ImmutableList 中不会发生。


8
我建议不可变集合最重要的特征在于通过创建新的不可变集合并使用底层高效的“写时复制”机制来操作它们。因此,对我来说,其他两个答案似乎更相关。 - koloman
1
在 C# .Net 中,不可变性至少是一个大谎言。我不确定其他编程语言。在 C# 中,即使使用 ImmutableList,任何人都可以通过非托管路线更改该内存位置的内容而不被 CLR 感知。但是只要所有各方遵守 .Net 平台提供的法律条款,我们就可以安全地认为 ImmutableList 确实是不可变的。 - RBT
39
任何程序都一样 - 如果你在同一个进程内运行代码,操作系统就不会阻止你更改任何内容。 - James Thorpe
即使是不可变集合,也可以通过修改底层集合来进行修改。我不明白为什么我们说这是不可能的? - Ashwini Mohan
如果您使用ImmutableList的正确公共API,则没有基础集合可供修改 - 当您调用Add等时,它会返回带有添加内容的集合的新副本 - 原始版本保持不变。对于ReadOnlyCollection,虽然您无法通过该类本身修改它,但仍然可以修改使用ReadOnlyCollection初始化的基础集合。如果您开始使用反射或其他修改数据的方法,那么是的 - 任何事都可以像我之前的评论一样。 - James Thorpe
没错,这就是我的理解。不可变列表的线程安全肯定是一个优点,但最终一切都是可修改的。 在只读列表中,只需要一个foreach循环,而在不可变列表中,需要另一个嵌套的foreach来进行修改。也许我期望过高了。 但还是感谢@JamesThorpe的解释。非常感激。 - Ashwini Mohan

39

ReadOnlyCollection<T>不能解决任何线程安全问题,它只是IList<T>的包装器。它不公开修改集合的成员,但您始终可以使用底层集合引用来修改它。

如果底层集合被修改,则枚举ReadOnlyCollection<T>不安全。如果这样做,您将获得相同的InvalidOperationException和消息“Collection was modified; enumeration operation may not execute...”。

来自ReadOnlyCollection<T>

  

ReadOnlyCollection可以支持多个读者同时访问,只要不修改集合。即使如此,枚举集合本质上不是线程安全的过程。为了保证枚举期间的线程安全性,您可以在整个枚举过程中锁定集合。要允许多个线程对其进行读写访问,必须实现自己的同步。

另一方面,ImmutableList是不可变的,因此本质上是线程安全的。


32

ReadOnlyCollection只能读取数据,正如其名称所示。

另一方面,您可以通过调用Add/Remove/Clear等方法来向ImmutableList添加/删除项目,这些方法将返回一个新的不可变列表。


24
我会补充一点,这使得 ImmutableList<> 非常类似于一个 string - xanatos
@xanatos 的确,或任何(正确实现的)值类型。 - dcastro
5
使用ImmutableList时,与字符串一样,重要的区别不在于你不能修改原始对象,而是创建了一个新对象来保存更改(例如使用Remove方法),但原始版本保持不变。 - Simon Vane

10
在多线程场景中,需要注意只读集合仍然不是线程安全的。
根据ReadOnlyCollection<T>文档:
“...如果对基础集合进行更改,则只读集合会反映这些更改。”
由于像List<T>和其他集合一样,不是线程安全的,因此只读集合也不是线程安全的。
重要提示:有一些边缘情况在MSDN中没有明确解释。看起来只读取集合内容的某些操作实际上修改了集合的内部结构。为什么没有指定这一点?一个显而易见的原因是因为这是一个不反映API的实现细节。结果是,即使您不修改包装在ReadOnlyCollection<T>中的List<T>,并且仅使用getter,在多线程环境中仍可能发生崩溃!
底线是常见的集合,即使包装在ReadOnlyCollection中,也不能直接在多线程环境中使用。
ReadOnlyCollection相反,不可变集合确保在获得对集合的引用之后,不会更改任何内部结构。请注意,这些结构仍然不是真正的不可变。它们代替是可冻结的。这意味着结构将在内部更改一段时间,直到被冻结并返回给调用者。超过该点后,对不可变集合的所有其他调用只会在原始引用中访问的结构之外进行修改。
结论:只读集合不是线程安全的;不可变集合是线程安全的。

3

到目前为止,所有的技术细节都讲解完毕,但是并没有代码。

我创建了一个小例子来展示主要区别。

考虑在这种情况下,将只读集合和不可变列表指向同一对象"字符串列表"

如果我们在只读集合创建后向"字符串列表"添加新项,我们可以看到只读集合被修改。(所以我们设法找到了一种改变只读集合的方法)

然而,当我们在不可变列表上采取相同的步骤时,不可变列表保持不变。

List<string> strings = new List<string>()
{
    "Visual Studio 2017","Visual Studio 2019"
};
ReadOnlyCollection<string> readOnlyStrings = strings.AsReadOnly();
var immutableList = strings.ToImmutableList();
strings.Add("Visual Studio 2022");
Console.WriteLine("Readonly List");

foreach (string item in readOnlyStrings)
{
    Console.WriteLine(item);
}
Console.WriteLine("Immutable List");

foreach (string item in immutableList)
{
    Console.WriteLine(item);
}

Screenshot


这行代码 strings.Add("Visual Studio 2022"); 没有任何效果,因为调用的结果被忽略了。 - Theodor Zoulias
1
你是否仔细检查了ss和代码示例?当然会影响,只读列表指向字符串列表,它被添加到只读列表中,如所示。 - erhan355
你是对的,我误读了代码。我以为是immutableList.Add("Visual Studio 2022");。你可以考虑在回答中添加更多文本,解释代码应该展示什么。目前它更像是一个谜题而不是一个有用的答案。 - Theodor Zoulias
1
@TheodorZoulias 好的,我认真听取你的建议;)会尝试改进答案。 - erhan355

1
主要的好处是内存效率,而不是线程安全。 System.Collections.Immutable 中的不可变集合被设计成允许修改版本的集合与相同集合的未修改副本共享数据结构。这些差异以聪明的方式编码,不需要复制整个数据结构。在 Immutable Collections 中有很好的解释。 ReadOnlyCollection 在每次进行修改时需要创建一个完全独立的集合副本。这会使用更多的内存并对垃圾收集器造成更高的负担。
正如您所观察到的,这两个类都试图保证集合不能有可观察的修改。只要它们不被误用,这两个类都能在一定程度上实现这一点。

ReadOnlyCollection在每次进行修改时需要创建一个完全独立的集合副本。”--为什么?为什么要创建一个副本,而不是直接修改集合呢?集合的所有者始终可以对其进行修改。只有只读外观的用户无法改变集合。” - Theodor Zoulias
拥有一个能够改变集合的单一所有者和多个只读用户,他们从不创建修改版本,这无疑是一种常见情况。然而,在其他一些情况下,只读用户将希望获取旧集合并创建一个新集合,添加、删除或修改元素。这第二种用例是不可变集合可能更有效的地方。 - andypea

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