ReadOnlyCollection 或 IEnumerable 用于公开成员集合?

Posted

技术标签:

【中文标题】ReadOnlyCollection 或 IEnumerable 用于公开成员集合?【英文标题】:ReadOnlyCollection or IEnumerable for exposing member collections? 【发布时间】:2010-10-04 05:33:40 【问题描述】:

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

谢谢/埃里克

【问题讨论】:

如果您使用的是 .NET 4.5,您可能想尝试新的 read-only collection interfaces。如果您偏执,您仍然希望将返回的集合包装在 ReadOnlyCollection 中,但现在您不依赖于特定的实现。 【参考方案1】:

更现代的解决方案

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

更新答案以更直接地解决问题

如果调用代码只对集合进行迭代,是否有任何理由将内部集合公开为 ReadOnlyCollection 而不是 IEnumerable?

这取决于您对调用代码的信任程度。如果您完全控制将调用此成员的所有内容,并且您保证不会使用任何代码:

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

那么当然,如果您直接归还收藏品,也不会造成任何伤害。不过,我通常会尝试变得更加偏执。

同样,正如你所说:如果你只需要 IEnumerable&lt;T&gt;,那为什么还要把自己绑在更强大的东西上?

原答案

如果您使用的是 .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;
    

【讨论】:

请注意,这样做会降低性能:AFAIK Enumerable.Count 方法针对转换为 IEnumerables 的集合进行了优化,但不适用于 Skip(0) 生成的范围 -1 是我自己还是这个问题的答案?了解这个“解决方案”很有用,但操作要求比较返回 ReadOnlyCollection 和 IEnumerable。这个答案已经假设您想要返回 IEnumerable 而没有任何理由支持该决定。 答案显示将ReadOnlyCollection 转换为ICollection,然后成功运行Add。据我所知,这应该是不可能的。 evil.Add(...) 应该抛出一个错误。 dotnetfiddle.net/GII2jW @ShaunLuttin:是的,正是 - 抛出。但在我的回答中,我没有投 ReadOnlyCollection - 我投了 IEnumerable&lt;T&gt; 这实际上不是只读的......它而不是公开ReadOnlyCollection 啊哈。你的偏执会导致你使用ReadOnlyCollection 而不是IEnumerable,以防止邪恶的铸造。【参考方案2】:

如果只需要遍历集合:

foreach (Foo f in bar.Foos)

然后返回 IEnumerable 就足够了。

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

Foo f = bar.Foos[17];

然后将其包装在 ReadOnlyCollection 中。

【讨论】:

@Vojislav Stojkovic,+1。这个建议今天仍然适用吗? @w0051977 这取决于您的意图。当你暴露一些在你得到参考后永远不会改变的东西时,按照 Jon Skeet 的建议使用 System.Collections.Immutable 是完全有效的。另一方面,如果你要公开一个可变集合的只读视图,那么这个建议仍然有效,尽管我可能会将它公开为IReadOnlyCollection(我写这篇文章时它不存在)。 @VojislavStojkovic 你不能将bar.Foos[17]IReadOnlyCollection 一起使用。唯一的区别是额外的Count 属性。 public interface IReadOnlyCollection&lt;out T&gt; : IEnumerable&lt;T&gt;, IEnumerable @Konrad 对,如果您需要bar.Foos[17],您必须将其公开为ReadOnlyCollection&lt;Foo&gt;。如果您想知道大小并对其进行迭代,请将其公开为IReadOnlyCollection&lt;Foo&gt;。如果您不需要知道大小,请使用IEnumerable&lt;Foo&gt;。还有其他解决方案和其他细节,但这超出了这个特定答案的讨论范围。【参考方案3】:

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

【讨论】:

我非常不同意这种“偏执的设计”。我认为选择不充分的语义来执行政策是不好的做法。 你不同意并没有错。如果我正在设计一个用于外部分发的库,我宁愿拥有一个偏执的界面,而不是处理试图滥用 API 的用户提出的错误。请参阅 msdn.microsoft.com/en-us/library/k2604h5s(VS.71).aspx 上的 C# 设计指南 是的,在为外部分发设计库的特定情况下,为您要公开的任何内容提供一个偏执的界面是有意义的。即便如此,如果 Foos 属性的语义是顺序访问,使用 ReadOnlyCollection 然后返回 IEnumerable。无需使用错误的语义;) 您也不需要这样做。您可以在不复制的情况下返回只读迭代器... @shojtsy 您的代码行似乎不起作用。 as 生成一个 null。强制转换引发异常。【参考方案4】:

我尽可能避免使用 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());

【讨论】:

过早优化。根据我的测量,迭代 ReadOnlyCollection 所花费的时间大约比常规列表长 36%,假设您在 for 循环内执行 nothing。很有可能,您永远不会注意到这种差异,但如果这是您代码中的一个热点,并且需要您可以从中挤出的每一点性能,为什么不使用数组来代替呢?那会更快。 您的基准测试包含缺陷,您完全忽略了分支预测。【参考方案5】:

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

【讨论】:

以上是关于ReadOnlyCollection 或 IEnumerable 用于公开成员集合?的主要内容,如果未能解决你的问题,请参考以下文章

ReadonlyCollection,对象是不可变的吗?

更新 GridView 中的 ReadOnlyCollection

readOnlycollection 的 .Item 属性不起作用

为啥 List.AsReadOnly 返回 ReadOnlyCollection 而 Dictionary.AsReadOnly 返回 IReadOnlyDictionary?

c# - 如何计算ReadOnlyCollection字段中字符串的出现次数[重复]

迭代时修改 Collection 底层的 ReadOnlyCollection 属性