如果调用代码仅迭代集合,是否有任何理由将内部集合公开为ReadOnlyCollection而不是IEnumerable?
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);
正如我所看到的,IEnumerable是ReadOnlyCollection接口的一个子集,它不允许用户修改集合。因此,如果IEnumberable接口足够,那么就是要使用的接口。这是推理它的正确方法还是我错过了什么?
谢谢/ Erik
答案 0 :(得分:91)
更现代的解决方案
除非您需要内部集合是可变的,否则您可以使用System.Collections.Immutable
包,将您的字段类型更改为不可变集合,然后直接公开它 - 假设Foo
本身是不可变的,当然。
更新了直接解决问题的答案
如果调用代码仅迭代集合,是否有任何理由将内部集合公开为ReadOnlyCollection而不是IEnumerable?
这取决于您对调用代码的信任程度。如果您完全控制将要调用此成员的所有内容,并且保证没有代码可以使用:
ICollection<Foo> evil = (ICollection<Foo>) bar.Foos;
evil.Add(...);
然后确定,如果您直接返回该集合,则不会造成任何伤害。我通常试图比那更偏执。
同样,如你所说:如果你只是需要 IEnumerable<T>
,那么为什么要把自己绑在更强大的东西上呢?
原始回答
如果您使用的是.NET 3.5,则可以通过使用对Skip的简单调用来避免复制和以避免简单转换:
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;
}
}
答案 1 :(得分:40)
如果您只需要遍历集合:
foreach (Foo f in bar.Foos)
然后返回 IEnumerable 就足够了。
如果您需要随机访问项目:
Foo f = bar.Foos[17];
然后将其包装在 ReadOnlyCollection 。
中答案 2 :(得分:30)
如果你这样做,那么没有什么能阻止你的调用者将IEnumerable转换回ICollection然后修改它。 ReadOnlyCollection消除了这种可能性,尽管仍然可以通过反射访问底层的可写集合。如果集合很小,那么解决这个问题的一种安全而简单的方法就是返回副本。
答案 3 :(得分:3)
我尽量避免使用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());
答案 4 :(得分:0)
有时您可能想要使用界面,可能是因为您想在单元测试期间模拟集合。请参阅我的blog entry,了解如何使用适配器将自己的接口添加到ReadonlyCollection。