微软基础类库(Base Class Library)团队已经完成了.NET不可变集合的正式版本,但不包括ImmutableArray。与其一起发布的还包括针对其它不可变对象类型的设计指南。
如果你需要在多个线程中安全地共享集合,并且允许每个线程在需要时对其内容进行改变。这种场景就是不可变集合所设计的初衷。只读集合在使用时需要复制集合中的全部内容,而新的不可变集合可以以一种更高性能的方式从一个现有集合中进行创建。
使用不可变集合需要特别当心,因为你很容易错误地写成“list.Add(item)”,而正确的方法是“list = list.Add(item)”。甚至编译器也可能产生类似的错误,这也是为什么不可变集合不支持构造函数的原因。考虑以下代码:
list = new ImmutableList<int> {1, 2, 3};
在编译后会产生以下代码:
temp = new ImmutableList(); temp.Add(1); temp.Add(2) temp.Add(3) list = temp;
由于3次Add方法的结果都被丢弃,最终整个集合包含的项数目为0,而不是期望中的3。
不可变对象指南
Immo Lendwerth建议,当你在创建自己的不可变对象时,在其中加入适当的WithXxx方法。对简单的对象来说,为每一个属性创建一个WithXxx方法即可。当属性值需要变化时,该方法会返回当前对象的一个拷贝。
如果某属性代表了一个结合,那么这种模式就需要一点变化。以下这段代码来自Immo的发布声明:
class Order { public Order(IEnumerable<OrderLine> lines) { Lines = lines.ToImmutableList(); } public ImmutableList<OrderLine> Lines { get; private set; } public Order WithLines(IEnumerableOrderLine> value) { return Object.ReferenceEquals(Lines, value) ? this : new Order(value); } }
如你所见,WithLines方法可接受任意IEnumerable。因此你可以传递一个新创建的ImmutableList对象,或者是某个LINQ表达式的结果。这种方式已经足以满足需求了,不过他还建议提供某些辅助方法:
class Order { //... public Order AddLine(OrderLine value) { return WithLines(Lines.Add(value)); } public Order RemoveLine(OrderLine value) { return WithLines(Lines.Remove(value)); } public Order ReplaceLine(OrderLine oldValue, OrderLine newValue) { return oldValue == newValue ? this : WithLines(Lines.Replace(oldValue, newValue)); } }
ImmutableArray被移除
由于性能方面的原因,ImmutableArray从最终的发布版本中被移除。其原因是:为了满足内存性能指标,ImmutableArray必须设计成一个值对象,并且为了保持值对象的语义,ImmutableArray的默认实例必须表现为一个空数组形式。不幸的是,为了达到这一点,对空值的检测(null check)会使得C#无法移除对数组边界的检测,而这一点是为达到良好CPU性能的一个重要考虑事项。
由于ImmutableArray类对于Roslyn编译器项目非常重要,设计者曾考虑删除会导致性能问题的空值检测功能,但又因此产生了另外的问题,Immo这样写道:
由于所有的值类型都有一个自动产生的默认构造函数,它会将该值类型初始化为它的默认状态,而ImmutableArray<T>的默认值是空,它的底层数组实现则为null。因此,AddRange方法的实现会因为NullReferenceException的产生而崩溃。
这一问题还表现在其它一些地方,由于ImmutableArray<T>实现了某些集合接口(例如IEnumerable和IReadOnlyList),因此你可以把它传递给某些接受这种接口的方法。由于这种接口引用是非空的,使用者在调用它的方法或者属性时不会考虑到有可能产生NullReferenceException。
基础类库团队并未放弃这个项目,他们还在研究其它设计方式,以争取让ImmutableArray重新亮相。