133

如果调用代码仅迭代集合,是否有任何理由将内部集合公开为 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 接口足够了,那么它就是可以使用的接口。这是一种正确的推理方式还是我错过了什么?

谢谢/埃里克

4

5 回答 5

100

更现代的解决方案

除非您需要内部集合是可变的,否则您可以使用该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); }
}

(还有很多其他选项可以简单地包装 - 关于SkipSelect/Where 的好处是没有代表可以为每次迭代毫无意义地执行。)

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

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}
于 2009-01-29T13:32:26.200 回答
46

如果您只需要遍历集合:

foreach (Foo f in bar.Foos)

然后返回IEnumerable就足够了。

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

Foo f = bar.Foos[17];

然后将其包装在ReadOnlyCollection中。

于 2009-01-29T12:32:53.947 回答
33

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

于 2009-01-29T12:28:06.313 回答
3

我尽可能避免使用 ReadOnlyCollection,它实际上比仅使用普通列表要慢得多。看这个例子:

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());
于 2010-06-10T05:38:21.973 回答
0

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

于 2009-02-11T15:25:37.987 回答