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


124

如果调用代码仅对集合进行迭代,是否有任何理由将内部集合公开为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


6
如果您使用的是.NET 4.5,则可能要尝试使用新的只读集合接口ReadOnlyCollection如果您很偏执,您仍然希望将返回的集合包装在a中,但是现在您不再局限于特定的实现了。
StriplingWarrior 2012年

Answers:


96

更现代的解决方案

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

15
请注意,这样做会降低性能:AFAIK Enumerable.Count方法针对强制转换为IEnumerables的Collections优化,但不适用于Skip(0)
shojtsy 2010年

6
-1就是我还是这是对另一个问题的答案?知道此“解决方案”很有用,但是操作员要求在返回ReadOnlyCollection和IEnumerable之间进行比较。该答案已经假定您要返回IEnumerable,而无需任何理由支持该决定。
Zaid Masud 2012年

1
答案显示将a强制转换ReadOnlyCollection为an ICollection,然后Add成功运行。据我所知,这是不可能的。会evil.Add(...)引发错误。dotnetfiddle.net/GII2jW
Shaun Luttin 2015年

1
@ShaunLuttin:是的,确实是这样。但是,在我的回答中,我不会投放ReadOnlyCollection-我投放的IEnumerable<T>内容实际上不是只读的... 而不是公开ReadOnlyCollection
乔恩·斯基特

1
啊哈 您的妄想症会导致您使用a ReadOnlyCollection而不是a IEnumerable,以防止恶意投放。
肖恩·卢丁

43

如果只需要遍历集合:

foreach (Foo f in bar.Foos)

然后返回IEnumerable就足够了。

如果您需要随机访问商品:

Foo f = bar.Foos[17];

然后将其包装在ReadOnlyCollection中


@Vojislav Stojkovic,+ 1。这个建议今天仍然适用吗?
w0051977

1
@ w0051977这取决于您的意图。使用System.Collections.Immutable如乔恩斯基特建议是完全有效的,当你暴露的东西是永远不会改变你对它的引用后。另一方面,如果您公开的是可变集合的只读视图,则此建议仍然有效,尽管我可能会将其公开为IReadOnlyCollection(我写此建议时不存在)。
Vojislav Stojkovic

您不能bar.Foos[17]与@VojislavStojkovic一起使用IReadOnlyCollection。唯一的区别是附加Count属性。 public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
康拉德

@Konrad对,如果需要bar.Foos[17],您必须将其公开为ReadOnlyCollection<Foo>。如果您想知道大小并进行迭代,请将其公开为IReadOnlyCollection<Foo>。如果您不需要知道大小,请使用IEnumerable<Foo>。还有其他解决方案和其他细节,但这超出了此特定答案的讨论范围。
Vojislav Stojkovic

31

如果执行此操作,则不会阻止调用方将IEnumerable强制转换回ICollection,然后对其进行修改。尽管仍然可以通过反射访问底层可写集合,但ReadOnlyCollection消除了这种可能性。如果馆藏很小,那么解决此问题的安全简便方法是返回副本。


14
我衷心不同意这种类型的“偏执设计”。我认为选择不恰当的语义来执行策略是一种不好的做法。
Vojislav Stojkovic,2009年

24
您不同意并没有错。如果我正在设计一个用于外部分发的库,我宁愿拥有一个偏执的界面,而不是处理试图滥用API的用户所引发的错误。见在C#设计指南msdn.microsoft.com/en-us/library/k2604h5s(VS.71).aspx
斯图Mackellar

5
是的,在设计用于外部分发的库的特定情况下,为暴露的内容提供一个偏执的接口是有意义的。即使这样,如果Foos属性的语义是顺序访问,请使用ReadOnlyCollection,然后返回IEnumerable。无需使用错误的语义;)
Vojislav Stojkovic

9
您也不需要这样做。您可以返回只读的迭代器而无需复制...
Jon Skeet

1
@shojtsy您的代码行似乎无效。 as生成null。强制转换引发异常。
尼克·阿列克谢耶夫

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());

15
过早的优化。根据我的测量,假设您在for循环内不进行任何操作,则对ReadOnlyCollection进行迭代所需的时间将比常规列表长约36%。通常,您永远不会注意到这种差异,但是,如果这是代码中的热点,并且需要您可以发挥其全部性能,那么为什么不使用Array呢?那会更快。
StriplingWarrior 2012年

您的基准测试包含缺陷,您完全忽略了分支预测。
Trojaner

0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.