【问题标题】:IEnumerable vs IReadonlyCollection vs ReadonlyCollection for exposing a list memberIEnumerable vs IReadonlyCollection vs ReadonlyCollection 用于公开列表成员
【发布时间】:2014-09-12 20:29:35
【问题描述】:

我花了好几个小时来思考暴露列表成员的问题。在与我类似的问题中,Jon Skeet 给出了一个很好的答案。请随意看看。

ReadOnlyCollection or IEnumerable for exposing member collections?

我通常对公开列表非常偏执,尤其是当您正在开发 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);
    }
}

任何针对 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);
    }
}

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

我发布这个问题是为了获得一些关于我的想法的意见。与 IEnumerable 相比,您更喜欢此解决方案吗?您认为使用 ReadOnlyCollection 的具体返回值更好吗?这是一场激烈的辩论,我想尝试看看我们都可以提出哪些优点/缺点。

提前感谢您的意见。

编辑

首先感谢大家为这里的讨论做出了如此多的贡献。我当然从每一个人身上学到了很多东西,并真诚地感谢你。

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

IReadOnlyCollection 和 IEnumerable 存在一些常见的缺陷。

考虑下面的例子:

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

即使接口是只读的,上面的示例也可以转换回列表并进行变异。该接口,尽管它是同名的,但并不能保证不变性。提供不可变的解决方案取决于您,因此您应该返回一个新的 ReadOnlyCollection。通过创建一个新列表(本质上是一个副本),您的对象的状态是安全可靠的。

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

示例如下:

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

上面的内容可以被强制转换和变异,但你的对象仍然是不可变的。

另一个框框语句是集合类。考虑以下几点:

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

上面的类可以有方法以你想要的方式改变 foo,但是你的对象永远不能被强制转换为任何类型的列表并被改变。

Carsten Führmann 对 IEnumerables 中的 yield return 语句提出了精彩的观点。

再次感谢大家。

【问题讨论】:

  • 嗯,我认为您应该阅读很棒的 Jon Skeet anwser:stackoverflow.com/questions/491375/… [可能重复]
  • 如果您从顶部阅读问题,您会发现我在我的问题中引用了 John Skeet 的回答作为类似问题。 :) 虽然答案写得很好,但我相信有一些话题没有完全涉及到这个
  • 没问题。很容易错过。 :)
  • 我无法理解 IReadOnlyCollection 如何永远无法转换为 List,我们不能这样做 WorkItems.AsEnumerable().ToList(); ??
  • 嗨,阿南思。好问题。自从我想到这个问题以来已经有一段时间了,从那以后我学到了很多东西。使用 WorkItmes.AsEnumerable().ToList() 将创建列表的副本,并且您的类不会发生变异。在上述情况下,由于返回了一个新的只读集合,您将永远无法将其转换回列表。您的演员表将为空 话虽如此,IReadOnlyCollection 存在一个常见的陷阱。如果您在 get 中返回列表,则有人可以将其转换为列表并改变对象的状态。为了清楚起见,我将更新问题

标签: c# .net list ienumerable encapsulation


【解决方案1】:

我对强制转换和 IReadOnly* 合同以及此类合同的“正确”用法的看法。

如果某些代码“聪明”到足以执行显式强制转换并破坏接口契约,那么它也“聪明”到足以使用反射或以其他方式执行恶意操作,例如访问 ReadOnlyCollection 的底层列表 包装器对象。 我不会针对这种“聪明”的程序员进行编程。

我唯一保证的是,说 IReadOnly*-interface 对象暴露之后,我的代码不会违反该合同并且不会修改返回的集合对象。

这意味着我编写的代码会返回 List-as-IReadOnly*,例如,并且很少选择实际的只读具体类型或包装器。使用 IEnumerable.ToList 足以返回 IReadOnly[List|Collection] - 调用 List.AsReadOnly 对仍然可以访问 ReadOnlyCollection 包装的底层列表的“聪明”程序员几乎没有价值/em>。

在所有情况下,我保证 IReadOnly* 返回值的具体类型是急切的。如果我曾经编写过返回 IEnumerable 的方法,那是因为该方法的契约是“支持流式传输”fsvo。

就 IReadOnlyList 和 IReadOnlyCollection 而言,当存在对索引有意义的“一个”隐含稳定排序建立时,我使用前者,而不管有目的的排序如何。例如,数组和列表可以作为 IReadOnlyList 返回,而 HashSet 最好作为 IReadOnlyCollection 返回。调用者总是可以根据需要将 I[ReadOnly]List 分配给 I[ReadOnly]Collection:这个选择是关于 contract 暴露的,而不是程序员,“聪明”或其他人会做什么。

【讨论】:

    【解决方案2】:

    !! IEnumerable 与 IReadOnlyList !!

    IEnumerable 从一开始就与我们同在。多年来,这是表示只读集合的​​事实上的标准方式。然而,从 .NET 4.5 开始,还有另一种方法可以做到这一点:IReadOnlyList。

    两个集合接口都很有用。

    【讨论】:

      【解决方案3】:

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

      IEnumerable&lt;T&gt;返回给调用者时,他们必须考虑返回的对象是“惰性流”的可能性,例如。以“收益回报”构建的集合。也就是说,每次使用 IEnumerable 时,调用者可能必须为生成 IEnumerable&lt;T&gt; 的元素而付出的性能损失。 (生产力工具“Resharper”实际上指出这是代码异味。)

      相比之下,IReadOnlyCollection&lt;T&gt; 向调用者发出信号,表示不会有惰性求值。 (Count 属性,与IEnumerable&lt;T&gt;Count extension method 相对(由IReadOnlyCollection&lt;T&gt; 继承,因此它也有方法),表明不懒惰。而且似乎有没有 IReadOnlyCollection 的惰性实现。)

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

      IReadOnlyCollection 所做的 是阻止调用者强制转换为其他集合类型。对于这种保护,必须使用 class ReadOnlyCollection&lt;T&gt;

      总之,IReadOnlyCollection&lt;T&gt; 相对于IEnumerable&lt;T&gt; 所做的唯一 事情是添加一个Count 属性,从而表明不涉及懒惰。

      【讨论】:

      • 不幸的是,IReadOnly* 中没有任何内容表明实现是否声称是不可变的,甚至是只读的(只读对象可以安全地与不允许更改的代码共享)一个对象,但可以查看其他人对对象所做的未来更改,该对象具有与其相关的“特殊”引用;许多实现 IReadOnly* 的类不符合该标准。
      • @Richiban:我称之为IReadableList&lt;T&gt;。我会保留IReadOnlyXX&lt;t&gt; 用于保证没有合法实现将暴露 突变手段的东西(暗示它们可以安全地与不允许修改底层集合的东西共享)但不会承诺不会以其他方式修改底层数据存储。
      • @supercat 是的,IReadableList 还不错……但IReadOnly* 根本没有任何意义,因为接口只能保证 可以 做什么,而不是它不能做的事情。
      • @nawfal 不幸的是,ICollection 和 IReadOnlyCollection 都继承自 IEnumerable,但不是相互继承。它们都提供了一个属性 Count,但有一个问题:LINQ 的 Count() 扩展方法针对 ICollection 而不是 IReadOnlyCollection 进行了优化。这是最不幸的。 github.com/microsoft/referencesource/blob/master/System.Core/…
      • @dfhwze 你的言论令人不安。我检查了微软的几个IReadOnlyCollection&lt;T&gt; 的衍生产品。幸运的是,它们都实现了ICollectionICollection&lt;T&gt;。尽管如此,微软还是给人们留下了一个陷阱,让人们编写自己的 IReadOnlyCollection&lt;T&gt; 派生词。
      【解决方案4】:

      谈到类库,我认为 IReadOnly* 真的很有用,我认为你做得对:)

      这都是关于不可变集合...在只有不可变和扩大数组之前是一项艰巨的任务,因此.net 决定在框架中包含一些不同的可变集合,为您实现丑陋的东西,但恕我直言他们没有为您提供非常有用的不可变的正确方向,尤其是在共享可变内容始终是 PITA 的高并发场景中。

      如果你检查今天的其他语言,比如objective-c,你会发现实际上规则完全颠倒了!他们总是在不同的类之间交换不可变集合,换句话说,接口只公开不可变的,并且在内部他们使用可变集合(是的,他们当然有),相反,如果他们想让外部人员更改集合,他们会公开适当的方法(如果该类是有状态的类)。

      因此,我在其他语言方面的一点经验让我觉得 .net 列表是如此强大,但不可变集合的存在出于某种原因 :)

      在这种情况下,不是帮助接口的调用者,避免他在更改内部实现时更改所有代码,就像使用 IList 与 List 一样,但使用 IReadOnly* 你正在保护您自己,您的班级,以不正确的方式使用,以避免无用的保护代码,有时您也无法编写的代码(过去在某些代码中,我不得不返回完整列表的克隆以避免这个问题)。

      【讨论】:

      • 非常好的答案。整个想法归结为一个词。封装。当我看到人们将列表作为具有 get 和 set 的属性公开时,我总是偏执。如此危险并保证会被滥用。其他语言如何处理不可变对象非常有趣
      • 谢谢!只是为了记录我在objective-c中所说的可变/不可变分别是NSArrayNSMutableArray,这个名字也能说话:)
      【解决方案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;
            }
          }
      ...
      

      由于workItems 字段实际上是List&lt;T&gt;,所以恕我直言,自然的想法是公开最广泛的接口,在这种情况下是IReadOnlyList&lt;T&gt;

      【讨论】:

      • 你不能像IEnumerable&lt;&gt;s 的问题中提到的那样将它转换回List&lt;&gt; 吗?
      • @Rawling:由于 ReadOnlyCollection 继承自 List,因此将其转换为列表会将其保留为 ReadOnlyCollection。这样做然后尝试添加元素将导致 NotSupportedException: Collection is read-only.
      • 另一种选择是使用 List.Skip(0) 返回 IEnumerable,而不是强制转换,以防止 IEnumerable 被显式强制转换回列表。见stackoverflow.com/a/491591/1025728
      • @JustinJStark ReadOnlyCollection 不涉及此处。 List&lt;T&gt; 隐藏在 IReadOnlyList&lt;T&gt; 界面后面,作为问题和您的第二条评论,您可以将其转换回 List&lt;T&gt;
      • 由于返回的值仍然可以转换为List&lt;T&gt; 并进行修改,我将添加对AsReadOnly 的调用并返回它。这将确保内部值不被修改。 get { return workItems.AsReadOnly(); }
      猜你喜欢
      • 2010-10-04
      • 2014-01-19
      • 2017-12-23
      • 1970-01-01
      • 2012-12-15
      • 2019-02-26
      • 1970-01-01
      • 1970-01-01
      • 2018-08-23
      相关资源
      最近更新 更多