.NET不可变集合已经正式发布

jopen 11年前

英文原文:.NET Immutable Collections Ready for Production

  微软基础类库(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 重新亮相。

来自: InfoQ