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

本文关键字:最佳 模式 是什么 集合 api 不可变 | 更新日期: 2023-09-27 17:49:31

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

public void DoSomeEnumerationsWithACollection(IEnumerable<Thing> collectionOfThings)
{ 
   foreach (var thing in collectionOfThings) doSomethingWith(thing);
   foreach (var thing in collectionOfThings) doSomethingElseWith(thing);
}

当然,这至少有两个缺点:

  1. API背后的代码不能依赖于collectionOfThings的不变性,可能会遇到"collection modified"异常或遇到其他微妙的问题。

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

我必须承认,除了使用命名约定之外,我从来没有找到一个令人满意的模式来解决这个问题。实用的方法似乎是IEnumerable是传递集合时摩擦最小的方法,尽管有缺点。

现在,使用不可变集合,情况得到了很大改善…

public void DoSomeEnumerationsWithACollection(ImmutableList<Thing> collectionOfThings)
{ 

不再有集合修改的风险,也不再存在多枚举对性能影响的模糊性。

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

理想情况下,我们可以使用像

这样的接口
public void DoSomeEnumerationsWithACollection(IImmutableEnumerable<Thing> collectionOfThings)

但是当然,接口不能强制像不可变性这样的语义,除非按照约定。

使用基类也可以

public void DoSomeEnumerationsWithACollection(ImmutableEnumerableBase<Thing> collectionOfThings)

只是创建非密封的不可变类被认为是不好的形式,以免子类引入可变性。无论如何,在BCL中还没有这样做。

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

所以…我的问题是,当传递不可变集合时,哪种模式被认为是最好的?一旦我们开始使用不可变性, immutableelist 是"新的IEnumerable"还是有更好的方法?

更新

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

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

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

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

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