最佳实践:如何公开只读的ICollection

10

我在我的类中有一个名为foosICollection<T>,我想将其公开为只读(请参见此问题)。我看到接口定义了一个属性.IsReadOnly,这似乎很合适...我的问题是:如何让类的使用者明确知道foos是只读的?

我不想依赖于他们记住在尝试未实现的方法(例如.Add())之前查询.IsReadOnly。理想情况下,我想将foos公开为ReadOnlyCollection<T>,但它没有实现IList<T>。我应该通过一个名为GetReadOnlyFooCollection的方法来公开foo,而不是通过属性来公开吗?如果是这样,那么这不会让某些人混淆并期望得到ReadOnlyCollection<T>吗?

这是C# 2.0,因此像ToList()这样的扩展方法不可用...

7个回答

14
你可以像这样将"foos"改为只读集合(ReadOnlyCollection):

ReadOnlyCollection<T> readOnlyCollection = foos.ToList<T>().AsReadOnly();

然后,您可以将其公开为类的属性。
编辑:
    class FooContainer
    {
        private ICollection<Foo> foos;
        public ReadOnlyCollection<Foo> ReadOnlyFoos { get { return foos.ToList<Foo>().AsReadOnly();} }

    }

注意:一旦获取了ReadOnlyFoos集合,它就不再与您的foos ICollection“同步”。请参阅您所引用的线程

4
ToList创建一个新的容器对象,并将所有的Foo引用复制到其中,如果foos很大,这将是一项昂贵的操作。由于ReadOnlyFoos是计算复杂度为O(N),根据类库指南的建议,更好的方法是创建一个名为GetFoosAsReadOnly()的方法。 - Edward Brey

4
自从这个问题被提出以来,.NET 4.0已经添加了一个>接口;最好使用它作为声明的返回类型。 然而,这也带来了一个问题,即要返回什么类型的实例。其中一种方法是复制原始集合中的所有项。 另一种方法是始终返回只读包装。 第三种方法是如果原始集合实现了IReadOnlyCollection,则返回原始集合。 每种方法在某些情况下都是最好的选择,但在其他情况下可能不理想(或者甚至很糟糕)。 不幸的是,Microsoft没有提供标准方法来询问两个非常重要的问题: 1.您承诺永远保持与您现在一样的项目吗?2.是否可以安全地直接将代码暴露给不会修改您内容的代码。 适当的包装风格取决于客户端代码期望使用其接收到的内容所做的事情。要避免的一些情况:1.提供了一个客户端将认为是不可变的类型,但是不是直接返回它,而是复制它,使用客户端无法识别为不可变的类型。因此,客户端被迫再次复制该集合。2.提供了一个客户端将认为是不可变的类型,但在返回之前,它被包装成客户端无法判断集合是否不可变的方式,因此被迫复制。3.由客户端提供了一个不应变异的可变类型(转换为只读接口),然后直接暴露给另一个客户端,后者确定它是可变类型,并继续修改它。4.收到对可变集合的引用,然后在将其返回给需要不可变对象的客户端之前封装在只读包装中。返回集合的方法承诺它是不可变的,因此客户端拒绝进行自己的防御性副本。然后,客户端无法做好集合可能发生更改的准备。对于对象可以从某些客户端接收并需要向其他客户端公开的集合,实际上没有特别“安全”的做法。始终复制所有内容在许多情况下是最安全的方法,但很容易导致一些情况,即不必完全复制的集合最终被复制了数百或数千次。按原样返回引用通常是最有效的方法,但它也可能在语义上存在危险。
我希望微软能够添加一种标准的方式,通过该方式可以询问集合中的上述问题,或者更好的是,要求它们生成其当前状态的不可变快照。一个不可变的集合可以非常廉价地返回其当前状态的不可变快照(只需返回自身),即使某些可变集合类型也可以以远低于完全枚举的代价返回不可变快照(例如,一个 List 可能由两个 T[][] 数组支持,其中一个持有对可共享的包含 256 个项的不可变数组的引用,另一个则持有对不可共享的可变数组的引用。制作列表的快照仅需要克隆自上次快照以来已被修改的项所在的内部数组,这可能比克隆整个列表要便宜得多。不幸的是,由于没有标准的“生成不可变快照”的接口(请注意,ICloneable 不算,因为可变列表的克隆将是可变的;虽然可以通过在只读包装器中封装可变克隆来制作不可变快照,但这仅适用于可克隆的东西,而即使不可克隆的类型也应支持“可变快照”函数)。

3
我的建议是直接返回使用ReadOnlyCollection<T>的结果。这可以明确告知调用者如何使用它。
通常情况下,我会建议使用适当的接口。但由于.NET Framework目前没有合适的IReadOnlyCollection,所以您必须使用ReadOnlyCollection类型。
此外,您必须注意使用ReadOnlyCollection时,因为它实际上并不是只读的:不可变性和ReadOnlyCollection

我很乐意返回一个ReadOnlyCollection:您是在建议我以包装器的方式自己实现剩下的IList方法吗? - Joel in Gö
1
ReadOnlyCollection即使在.NET 2.0中也实现了IList:http://msdn.microsoft.com/en-us/library/ms132474(VS.80).aspx - Don Kirkby
正是——我的ICollection接口不具备这个功能,所以如果我想将它作为IList传递出去,我需要在一个新的包装类中实现剩余的方法。请参见我在原问题中提到的主题。 - Joel in Gö

0
有时候你可能想要使用一个接口,比如你想在单元测试中模拟集合。请参考我的博客文章,了解如何通过使用适配器将自己的接口添加到ReadonlyCollection中。

0

我通常会返回一个 IEnumerable<T>

一旦你将集合设置为只读(因此像 AddRemoveClear 这样的方法不再起作用),那么集合支持的功能与可枚举对象相比就没多少了 - 我认为只有 CountContains

如果你的类的使用者确实需要像处理集合一样处理元素,那么将 IEnumerable 传递给 List<T> 的构造函数就很容易了。


0

我似乎已经决定返回IEnumerable并且对象被克隆。

public IEnumerable<Foose> GetFooseList() {
   foreach(var foos in Collection) {
     yield return foos.Clone();
   }
}
  • 在Foos上需要一个克隆方法。

这样可以保证集合中的内容不会被更改。请记住,ReadonlyCollection是“有漏洞”的,因为其中的对象可以像另一篇帖子中提到的那样被更改。


-4

返回一个 T[] 数组:

private ICollection<T> items;

public T[] Items
{
    get { return new List<T>(items).ToArray(); }
}

1
有关为什么这是一个不好的想法,请参见:http://blogs.msdn.com/ericlippert/archive/2008/09/22/arrays-considered-somewhat-harmful.aspx - Jacob Carpenter
我同意这是一个不好的想法。 - Jamie Penney
这不是只读的,而是数组中对象的副本。此外,返回数据数组只是一个糟糕的想法。 - Jon Tackabury

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接