.NET 开发中一个常见的误区是,认为 IEnumerable 或 ReadOnlyCollectionl 类型的变量是线程安全的。微软的 Andrew Arnott 解释道:
如果有人给你一个 ReadOnlyCollection
、IReadOnlyList 或 IEnumerable ,唯一能保证的是你无法修改它们的数据,但却不能保证给你集合的人不会修改。不过对于数据不会改变这点,你往往需要一些信心。这些类型在改变内容时不会提供事件通知,并且如果在你迭代其内容的时候,集合真的改变了,这是否会发生在不同的线程呢?这种行为将造成应用程序中的数据崩溃和随机异常。
要在你想用 IEnumerable 或 ReadOnlyCollection 的地方提供真正线程安全的集合,微软 BCL 小组提供了一组不可变集合的预览版。基于函数式编程的技术,对于那些通常会改变集合的方法将会创建一个新集合来代替。为了提高效率,可能会在新旧集合之间进行数据共享。
不可变集合一个有意思的特点是没有公共构造函数。相反,它们总是以 ImmutableXxx
使用静态 Empty 属性与传统模式相背离,但这么做是有原因的。构造函数必须要分配一个对象,但由于这些集合的不可变性,新的对象永远只能表示空的列表。鉴于修改集合会创建新的集合,因此使用构造函数会导致创建多个表示空列表的对象,这显然是种浪费。静态属性返回空列表的单例,应用程序中的所有代码都可以共享该单例。
建造者和集合
由于内存分配,构建一个不可变集合是相当昂贵的。我们已经在字符串(即字符的不可变集合)身上看到了这一点。为了缓解这一点,不可变集合将暴露一个 ToBuilder 方法,返回一个可以廉价修改的建造者对象。得到之后,还可以简单地使用 ToImmutable 再次得到不可变集合。
性能
不可变集合的性能可能会非常棘手。如下表所示,大多数不可变集合的时间复杂度都相当不错,并且是稳定的,不必担心在集合内部数组的最大尺寸会触发集合的完全拷贝。与普通集合不同,不可变集合中的项如果被移除,将会释放不用的空间。
但还是会有成本。每个操作都要在内存中分配另一个对象,给 GC 带来压力。最大的胜利是在创建集合的快照拷贝时。但最终的建议仍然是:使用最简单的代码完成工作,并在必要的时候调整性能。 该预览版可通过 Microsoft.Bcl.Immutable NuGet 包下载。
查看英文原文:.NET Goes Immutable
评论