在不变性之前,IEnumerable是许多 API 中的首选接口,因为它的优点是 API 对传递对象的实际类型不敏感。
public void DoSomeEnumerationsWithACollection(IEnumerable<Thing> collectionOfThings)
{
foreach (var thing in collectionOfThings) doSomethingWith(thing);
foreach (var thing in collectionOfThings) doSomethingElseWith(thing);
}
当然,这至少有两个缺点:
API 背后的代码不能依赖collectionOfThings的不变性,并且可能会遇到“集合修改”异常或遇到其他其他微妙问题。
我们不知道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 并使用命名约定来明确我们的代码依赖于传入的不可变集合。
所以......我的问题是在传递不可变集合时哪些模式被认为是最好的?一旦我们开始使用不可变性, ImmutableList是“新的IEnumerable ”还是有更好的方法?
更新
IReadOnlyCollection(下面的 Yuval Itzchakov 建议)是对IEnumerable的明显改进,但仍不能完全保护消费者免受集合中不受控制的更改。值得注意的是,Roslyn 代码库大量使用了不可变性(主要通过ImmutableArray),并且在将这些传递给其他方法时似乎使用了显式类型,尽管有几个位置ImmutableList被传递到接受IEnumerable的方法中。