用于公开成员集合的 ReadOnlyCollection 或 IEnumable?

如果调用代码只在集合上迭代,是否有理由将内部集合公开为 ReadOnlyCollection 而不是 IEnumable?

class Bar
{
private ICollection<Foo> foos;


// Which one is to be preferred?
public IEnumerable<Foo> Foos { ... }
public ReadOnlyCollection<Foo> Foos { ... }
}




// Calling code:


foreach (var f in bar.Foos)
DoSomething(f);

如我所见,IEnumable 是 ReadOnlyCollection 接口的子集,它不允许用户修改集合。因此,如果 IEnumberable 接口足够了,那么它就是要使用的接口。这样说合适吗,还是我漏掉了什么?

谢谢/艾瑞克

38935 次浏览

如果您这样做,那么就没有什么能够阻止您的调用者将 IEnumable 转换回 ICollection 并修改它。ReadOnlyCollection 消除了这种可能性,尽管仍然可以通过反射访问底层的可写集合。如果集合很小,那么解决这个问题的一个安全而简单的方法就是返回一个副本。

如果只需要遍历集合:

foreach (Foo f in bar.Foos)

那么返回 数不胜数就足够了。

如果您需要随机访问项目:

Foo f = bar.Foos[17];

然后用 ReadOnlyCollection包起来。

更现代的解决方案

除非您需要内部集合是可变的,否则可以使用 System.Collections.Immutable包,将字段类型更改为不可变集合,然后直接公开该集合——当然,前提是 Foo本身是不可变的。

更新答案以更直接地回答问题

如果调用代码只在集合上迭代,是否有理由将内部集合公开为 ReadOnlyCollection 而不是 IEnumable?

这取决于您对调用代码的信任程度。如果你完全控制了所有将要调用这个成员的东西,而且你的 保证没有任何代码会使用:

ICollection<Foo> evil = (ICollection<Foo>) bar.Foos;
evil.Add(...);

那么当然,如果你只是直接返回收集不会造成任何伤害。不过我通常会试着多想一点。

同样,如你所说: 如果你只有 需要 IEnumerable<T>,那么为什么要把自己绑在任何更强的东西上?

原始答案

如果你吸毒的话。NET 3.5,你可以避免复制 还有避免简单的强制转换通过使用一个简单的调用跳过:

public IEnumerable<Foo> Foos {
get { return foos.Skip(0); }
}

(还有很多其他的选项可以进行简单的包装—— Skip优于 Select/Where 的好处是,每次迭代都没有无意义的委托执行。)

如果你不使用.NET 3.5,你可以编写一个非常简单的包装器来做同样的事情:

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
foreach (T element in source)
{
yield return element;
}
}

有时您可能想要使用一个接口,也许是因为您想在单元测试期间模拟集合。请参阅我的 博客日志,了解如何使用适配器将您自己的接口添加到 ReadonlyCollection。

我尽量避免使用 ReadOnlyCollection,它实际上比使用普通 List 慢得多。 看这个例子:

List<int> intList = new List<int>();
//Use a ReadOnlyCollection around the List
System.Collections.ObjectModel.ReadOnlyCollection<int> mValue = new System.Collections.ObjectModel.ReadOnlyCollection<int>(intList);


for (int i = 0; i < 100000000; i++)
{
intList.Add(i);
}
long result = 0;


//Use normal foreach on the ReadOnlyCollection
TimeSpan lStart = new TimeSpan(System.DateTime.Now.Ticks);
foreach (int i in mValue)
result += i;
TimeSpan lEnd = new TimeSpan(System.DateTime.Now.Ticks);
MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
MessageBox.Show("Result: " + result.ToString());


//use <list>.ForEach
lStart = new TimeSpan(System.DateTime.Now.Ticks);
result = 0;
intList.ForEach(delegate(int i) { result += i; });
lEnd = new TimeSpan(System.DateTime.Now.Ticks);
MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
MessageBox.Show("Result: " + result.ToString());