IEnumerable vs IReadonlyCollection vs ReadonlyCollection用于公开列表成员

Mar*_*ier 59 .net c# ienumerable encapsulation list

我花了好几个小时思考暴露名单成员的主题.在与我的类似问题中,John Skeet给出了一个很好的答案.请随便看看.

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

我通常非常偏执暴露列表,特别是如果您正在开发API.

我一直使用IEnumerable来公开列表,因为它非常安全,而且它提供了很大的灵活性.我在这里举一个例子

public class Activity
{
    private readonly IList<WorkItem> workItems = new List<WorkItem>();

    public string Name { get; set; }

    public IEnumerable<WorkItem> WorkItems
    {
        get
        {
            return this.workItems;
        }
    }

    public void AddWorkItem(WorkItem workItem)
    {
        this.workItems.Add(workItem);
    }
}
Run Code Online (Sandbox Code Playgroud)

任何针对IEnumerable进行编码的人都非常安全.如果我后来决定使用有序列表或其他东西,它们的代码都没有中断,它仍然很好.这样做的缺点是IEnumerable可以被强制转换回这个类之外的列表.

出于这个原因,许多开发人员使用ReadOnlyCollection来公开成员.这是非常安全的,因为它永远不会被强制转换为列表.对我来说,我更喜欢IEnumerable,因为它提供了更大的灵活性,我是否想要实现与列表不同的东西.

我想出了一个我更喜欢的新想法.使用IReadOnlyCollection

public class Activity
{
    private readonly IList<WorkItem> workItems = new List<WorkItem>();

    public string Name { get; set; }

    public IReadOnlyCollection<WorkItem> WorkItems
    {
        get
        {
            return new ReadOnlyCollection<WorkItem>(this.workItems);
        }
    }

    public void AddWorkItem(WorkItem workItem)
    {
        this.workItems.Add(workItem);
    }
}
Run Code Online (Sandbox Code Playgroud)

我觉得这保留了IEnumerable的一些灵活性,并且封装得非常好.

我发布了这个问题,以便对我的想法有所了解.你更喜欢IEnumerable这个解决方案吗?你认为使用ReadOnlyCollection的具体返回值更好吗?这是一个相当大的争论,我想试着看看我们都能提出的优点/缺点是什么.

提前感谢您的意见.

编辑

首先,感谢大家为此次讨论做出的贡献.我确实从每一个人那里学到了很多东西,并真诚地感谢你.

我正在添加一些额外的场景和信息.

IReadOnlyCollection和IEnumerable存在一些常见的陷阱.

考虑以下示例:

public IReadOnlyCollection<WorkItem> WorkItems
{
    get
    {
        return this.workItems;
    }
}
Run Code Online (Sandbox Code Playgroud)

即使接口是只读的,上面的示例也可以返回到列表并进行变异.界面,尽管它是同名的,但并没有增加不变性.它由你来提供一个不可变的解决方案,因此你应该返回一个新的ReadOnlyCollection.通过创建一个新列表(本质上是一个副本),您的对象的状态是安全的.

Richiban在他的评论中说得最好:界面只能保证某些东西可以做什么,而不是它不能做什么

请参阅下面的示例.

public IEnumerable<WorkItem> WorkItems
{
    get
    {
        return new List<WorkItem>(this.workItems);
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的内容可以被渲染和变异,但你的对象仍然是不可变的.

另外一个框外声明将是集合类.考虑以下:

public class Bar : IEnumerable<string>
{
    private List<string> foo;

    public Bar()
    {
        this.foo = new List<string> { "123", "456" };
    }

    public IEnumerator<string> GetEnumerator()
    {
        return this.foo.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的类可以有按照你想要的方式改变foo的方法,但你的对象永远不能被转换为任何排序和变异的列表.

CarstenFührmann对IEnumerables中的收益率报表提出了一个很好的观点.

再次感谢大家.

Car*_*ann 68

到目前为止,答案中似乎缺少一个重要方面:

当a IEnumerable<T>返回给调用者时,他们必须考虑返回的对象是"懒惰流"的可能性,例如使用"yield return"构建的集合.也就是说IEnumerable<T>,对于IEnumerable的每次使用,可能必须由调用者支付产生元素的性能损失.(生产力工具"Resharper"实际上将其指出为代码气味.)

相比之下,IReadOnlyCollection<T>向呼叫者发出的信号是没有懒惰的评估.(该Count属性,而不是在Count 扩展方法IEnumerable<T>(这是由遗传IReadOnlyCollection<T>因此它具有方法为好),信号的非lazyness.等确实存在似乎是IReadOnlyCollection不懒惰实现的事实).

这对于输入参数也是有效的,因为请求IReadOnlyCollection<T>代替IEnumerable<T>该方法需要在集合上多次迭代的信号.当然,该方法可以从中创建自己的列表IEnumerable<T>并对其进行迭代,但由于调用者可能已经有一个已加载的集合,因此尽可能利用它是有意义的.如果调用者只有一个IEnumerable<T>,他只需要添加.ToArray().ToList()参数.

什么IReadOnlyCollection也不会做的是防止来电投其他一些集合类型.为了这种保护,人们必须使用该类 ReadOnlyCollection<T>.

总之,只有事情IReadOnlyCollection<T>确实相对IEnumerable<T>的就是添加一个Count属性,从而指示没有lazyness参与.

  • 不幸的是,IReadOnly*中没有任何内容表明实现声称是不可变的,甚至是只读的(只读对象可以安全地与代码共享,不能允许更改对象,但是允许查看具有与之相关的"特殊"引用的其他人所做的对象的未来更改;许多实现IReadOnly*的类不符合该标准. (3认同)
  • @supercat 是的,`IReadableList` 还不错......但是`IReadOnly*` 根本没有任何意义,因为接口只保证*可以*做的事情,而不是*不能*做的事情。 (3认同)
  • @nawfal不幸的是,ICollection &lt;T&gt;和IReadOnlyCollection &lt;T&gt;都继承自IEnumerable &lt;T&gt;,但彼此不继承。它们都提供了一个_Count_属性,但要注意的是:LINQ的_Count()_扩展方法针对ICollection &lt;T&gt;(而非IReadOnlyCollection &lt;T&gt;)进行了优化。这是最不幸的。https://github.com/microsoft/referencesource/blob/master/System.Core/System/Linq/Enumerable.cs (2认同)
  • @dfhwze 你的言论令人不安。我检查了几个 Microsoft 的“IReadOnlyCollection&lt;T&gt;”衍生产品。幸运的是,它们都实现了“ICollection”或“ICollection&lt;T&gt;”。尽管如此,微软还是为那些编写自己的“IReadOnlyCollection&lt;T&gt;”衍生产品的人留下了陷阱。 (2认同)

Mic*_*nny 18

谈到类库,我认为IReadOnly*非常有用,我认为你做得对:)

这一切都是关于不可变的集合...之前只有不可变的和放大数组是一项艰巨的任务,所以.net决定在框架中包含一些不同的,可变的集合,为你实现丑陋的东西,但恕我直言他们没有'给你一个非常有用的不可变方向,特别是在高并发场景中,共享可变东西总是PITA.

如果您今天检查其他语言,例如objective-c,您会看到实际上规则完全颠倒了!他们总是在不同的类之间交换不可变的集合,换句话说,接口公开只是不可变的,并且在内部他们使用可变集合(是的,他们当然有它),而是他们暴露正确的方法,如果他们想让外人改变集合(如果班级是有状态的班级).

所以我用其他语言获得的这种小经验促使我认为.net列表是如此强大,但不可变的集合出于某种原因:)

在这种情况下,不是帮助接口的调用者,以避免他改变所有代码,如果你改变内部实现,就像IList vs List一样,但是使用IReadOnly*你保护自己,你的类,以不正确的方式使用,以避免无用的保护代码,有时你不能写的代码(在过去的某些代码中我必须返回完整列表的克隆以避免这个问题) .


use*_*740 15

我对铸造和 IReadOnly* 合约及其“正确”用法的看法。

\n

如果某些代码 \xe2\x80\x9cclever\xe2\x80\x9d 足以执行显式转换并破坏接口契约,那么 \xe2\x80\x9cclever\xe2\x80\x9d 也足以使用反射或否则会做一些邪恶的事情,例如访问 ReadOnlyCollection包装对象的基础列表。我不针对这样的\xe2\x80\x9cclever\xe2\x80\x9d 程序员进行\xe2\x80\x99t 编程。

\n

我唯一保证的是,公开所述 IReadOnly* 接口对象之后,我的代码不会违反该约定,也不会修改返回的集合对象。

\n

这意味着我编写的代码返回 List-as-IReadOnly*,例如,很少选择实际的只读具体类型或包装器。使用 IEnumerable.ToList足以返回 IReadOnly[List|Collection] - 调用 List.AsReadOnly 对于仍然可以访问 ReadOnlyCollection 包装的基础列表的 \xe2\x80\x9cclever\xe2\x80\x9d 程序员来说几乎没有什么价值

\n

在所有情况下,我保证 IReadOnly* 返回值的具体类型是渴望的。如果我曾经编写一个返回 IEnumerable 的方法,那是因为该方法的约定是 \xe2\x80\x9c 支持流式传输\xe2\x80\x9d fsvo。

\n

就 IReadOnlyList 和 IReadOnlyCollection 而言,当存在对索引有意义的隐含稳定排序时,我使用前者,无论有目的的排序如何。例如,数组和列表可以作为 IReadOnlyList 返回,而 HashSet 最好作为 IReadOnlyCollection 返回。调用者总是可以根据需要将 I[ReadOnly]List 分配给 I[ReadOnly]Collection:此选择与公开的合约有关,而不是程序员 \xe2\x80\x9cclever\xe2\x80\x9d 或其他人将要执行的选择做。

\n


Dmi*_*nko 5

看来你可以只返回一个合适的接口

...
    private readonly List<WorkItem> workItems = new List<WorkItem>();

    // Usually, there's no need the property to be virtual 
    public virtual IReadOnlyList<WorkItem> WorkItems {
      get {
        return workItems;
      }
    }
...
Run Code Online (Sandbox Code Playgroud)

由于workItems字段实际上是List<T>如此自然的想法恕我直言是公开最宽的接口,这是IReadOnlyList<T>在这种情况下

  • 你不能把它转换回像`IEnumerable&lt;&gt;`s的问题中提到的`List&lt;&gt;`吗? (5认同)
  • 由于返回的值仍然可以转换为 `List&lt;T&gt;` 并进行修改,我会添加对 `AsReadOnly` 的调用并返回它。这将确保内部值不被修改。`get { return workItems.AsReadOnly(); }` (3认同)
  • @JustinJStark `ReadOnlyCollection` 不涉及这里。在`IReadOnlyList&lt;T&gt;` 接口后面隐藏了一个`List&lt;T&gt;`,作为问题和您的第二条注释,您可以将其转换回`List&lt;T&gt;`。 (2认同)