跨API传递不可变集合的最佳模式是什么

Nei*_*len 12 .net c# immutability immutablelist immutable-collections

在不可变性之前,IEnumerable是许多API中的首选接口,因为它具有API对传递对象的实际类型不敏感的优点.

public void DoSomeEnumerationsWithACollection(IEnumerable<Thing> collectionOfThings)
{ 
   foreach (var thing in collectionOfThings) doSomethingWith(thing);
   foreach (var thing in collectionOfThings) doSomethingElseWith(thing);
}
Run Code Online (Sandbox Code Playgroud)

当然,至少有两个缺点:

  1. API背后的代码不能依赖collectionOfThings的不变性,可能会遇到"集合修改"异常或遇到其他其他微妙问题.

  2. 我们不知道collectionOfThings是真实集合还是简单的延迟查询.如果我们假设它是一个真正的集合,那么我们就不会通过运行多个枚举来降低性能.如果我们假设它是一个延迟查询并且它实际上是一个真实的集合,那么将其转换为本地列表或其他冻结集合会产生不必要的成本,尽管它确实有助于保护我们免受第一个问题的影响(执行"ToList"操作时仍存在竞争条件).显然,我们可以编写少量代码来检查这一点,并尝试做"正确的事情",但这是令人讨厌的额外混乱.

我必须承认,除了使用命名约定之外,我从未找到过令人满意的模式来解决这个问题.尽管存在缺点,但是实用的方法似乎是IEnumerable是传递集合的最低摩擦方法.

现在,随着不可变的收藏,情况得到了很大改善......

public void DoSomeEnumerationsWithACollection(ImmutableList<Thing> collectionOfThings)
{ 
Run Code Online (Sandbox Code Playgroud)

不再存在收集修改的风险,并且对多个枚举的性能影响没有任何歧义.

但是,我们显然失去了API的灵活性,因为我们现在必须传入一个ImmutableList.如果我们的客户端有其他类型的可枚举不可变集合,则必须将其复制到ImmutableList中才能被使用,即使我们想要做的只是枚举它.

理想情况下,我们可以使用类似的界面

public void DoSomeEnumerationsWithACollection(IImmutableEnumerable<Thing> collectionOfThings)
Run Code Online (Sandbox Code Playgroud)

但是,当然,除了约定之外,接口不能强制执行不可变性等语义.

使用基类可能会起作用

public void DoSomeEnumerationsWithACollection(ImmutableEnumerableBase<Thing> collectionOfThings)
Run Code Online (Sandbox Code Playgroud)

除了它被认为是创建未密封的不可变类的不良形式,以免子类引入可变性.无论如何,这还没有在BCL中完成.

或者我们可以继续在API中使用IEnumerable并使用命名约定来明确我们的代码依赖于传递的不可变集合.

所以...我的问题是在传递不可变集合时哪些模式被认为是最好的?是ImmutableList "新的IEnumerable "一旦我们开始使用不可变性,或是否有更好的办法?

更新

IReadOnlyCollection(由Yuval Itzchakov建议)是对IEnumerable的明显改进,但仍然没有完全保护消费者免受集合中不受控制的变化的影响.值得注意的是,Roslyn代码库大量使用不变性(主要通过ImmutableArray)并且在将这些传递给其他方法时似乎使用显式类型,尽管有几个位置将ImmutableList传递给接受IEnumerable的方法.

Nei*_*len 5

经过几年大量使用不可变集合后,我已经决定几乎在所有地方都使用 ImmutableArray 的惯例。这并没有满足 API 灵活性的最初愿望,但在实践中,我很少使用 ImmutableList 或其他类似列表的结构,而且当我这样做时,调用 ToImmutableArray 来通过接口获取数据通常不会有太多开销。

  • @GoodNightNerdPride 这是有关 ImmutableArray 与 ImmutableList 性能的链接:https://www.infoq.com/news/2013/06/ImmutableArray/ ImmutableArray&lt;T&gt; 一点也不复杂。它实际上只是一个包装数组的结构。根据 ildasm 的说法,没有任何其他字段。这意味着从不可变数组中读取数据的时间复杂度为 O(1)。相反,添加到不可变数组需要底层数组的完整副本,使其成为 O(n) 操作。 (2认同)

Yuv*_*kov 1

在传递不可变集合时,什么模式被认为是最好的?

我认为你的问题的答案是IReadOnlyCollection<T>,它将在.NET 4.6中传播。通过传递只读集合,您既可以保持不变性,又可以使用实现该接口的常规集合。

  • 如果我提供一个不可变列表,消费者_不能_改变我的副本,而我_不能_改变消费者副本。如果我向消费者提供 ROC,它可以保护我免遭他们修改我的副本。但是,如果我在后台线程上调用消费者,然后改变 ROC 的底层集合,那么我_已经_更改了消费者副本。遗憾的是,在 API 中使用 IROC 会允许出现这种情况。 (3认同)
  • 谢谢尤瓦尔。这似乎是一个不错的建议,尽管我想知道它在强制执行不变性方面到底有多好。ReadOnlyCollection 显然实现了 IReadOnlyCollection 接口,但 MSDN 文档 (https://msdn.microsoft.com/en-us/library/ms132474(v=vs.110).aspx) 明确指出 ReadOnlyCollection 只是可变对象的包装器目的。因此,使用 IReadOnlyCollection 似乎仍然允许通过 API 传递可变对象。 (2认同)
  • IROC 的提供者正在有效地保护自己,防止消费者无意中改变集合。然而,消费者却得不到同样的保证。从这个意义上说,它的契约比完全不可变的 API 更弱,后者为消费者和提供者提供了保证。 (2认同)