.net 用于公开列表成员的IEnumerable与IReadonlyCollection与ReadonlyCollection

o3imoua4  于 2022-12-20  发布在  .NET
关注(0)|答案(5)|浏览(162)

我花了不少时间思考揭露名单成员的问题。在一个与我类似的问题中,乔恩·斯基特给出了一个出色的答案。请随意看一看。
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中的收益率回报陈述提出了一个奇妙的观点。

jk9hmnmh

jk9hmnmh1#

到目前为止,这些答案似乎缺少一个重要方面:
IEnumerable<T>被返回给调用者时,他们必须考虑返回的对象是“惰性流”的可能性,例如用“yield return”构建的集合。也就是说,产生IEnumerable<T>的元素的性能损失可能必须由调用者来支付,(生产力工具“Resharper”实际上指出这是一种代码气味。)
相比之下,IReadOnlyCollection<T>向调用者发出信号,表示将不进行延迟求值(Count属性与IEnumerable<T>Countextension method相反(IReadOnlyCollection<T>继承了Countextension method,因此它也具有该方法),它发出非延迟信号,而且似乎不存在IReadOnlyCollection的延迟实现这一事实也是如此)。
这对于输入参数也是有效的,因为请求IReadOnlyCollection<T>而不是IEnumerable<T>表示该方法需要在集合上迭代多次。当然,该方法可以从IEnumerable<T>创建自己的列表并在该列表上迭代,但是由于调用者可能已经具有加载的集合在手边,因此只要可能就利用它是有意义的。如果调用者手头只有一个IEnumerable<T>,他只需要将.ToArray().ToList()添加到参数中。
IReadOnlyCollection没有做的是防止调用方强制转换为其他集合类型。要实现这样的保护,必须使用ReadOnlyCollection<T>
总而言之,IReadOnlyCollection<T>相对于IEnumerable<T>所做的 * 唯一 * 事情是添加了一个Count属性,从而表明不涉及懒惰。

jv2fixgn

jv2fixgn2#

谈到类库,我认为IReadOnly* 非常有用,而且我认为您做得很对:)
这都是关于不可变集合的...以前只有不可变集合,扩大数组是一项艰巨的任务,所以.net决定在框架中包含一些不同的东西,可变集合,为你实现丑陋的东西,但恕我直言,他们没有给予你一个正确的方向不可变,这是非常有用的,特别是在一个高并发的情况下,共享可变的东西总是一个PITA。
如果你检查一下今天的其他语言,比如objective-c,你会发现实际上规则完全颠倒了!它们总是在不同的类之间交换不可变集合,换句话说,接口只暴露不可变,而在内部使用可变集合(是的,他们当然有),相反,如果他们想让外部人员更改集合(如果类是有状态类),他们会公开适当的方法。
因此,我在其他语言中获得的这一点经验促使我认为.net列表是如此强大,但不可变集合的存在是出于某种原因:)
在这种情况下,并不是帮助接口的调用者,避免他在更改内部实现时更改所有代码,就像IList vs List一样,而是使用IReadOnly*,您可以保护自己和类,避免以不正确的方式使用,避免无用的保护代码,有时候你也写不出来的代码(在过去的一些代码中,我不得不返回一个完整列表的克隆来避免这个问题)。

hrirmatl

hrirmatl3#

我对强制转换和IReadOnly* 合同的关注,以及这些合同的“正确”用法的看法。
如果某些代码足够“聪明”,能够执行显式强制转换并打破接口契约,那么它也足够“聪明”,能够使用反射或以其他方式做一些邪恶的事情,比如访问ReadOnlyCollection wrapper 对象的底层List。我不会与这样“聪明”的程序员竞争。
我唯一能保证的是,在公开了IReadOnly接口对象之后,我的代码不会违反该契约,也不会修改返回的集合对象。
这意味着我编写的代码返回List-as-IReadOnly*,例如,很少选择实际的只读具体类型或 Package 器。使用IEnumerable.ToList * 足以 * 返回IReadOnly[List| Collection] -调用List.AsReadOnly对“聪明的”程序员来说没有什么价值,因为他们仍然可以访问ReadOnlyCollection * Package * 的底层列表。
在任何情况下,我都保证IReadOnly* 返回值的具体类型是渴望的。如果我写了一个返回IEnumerable的方法,那是因为该方法的契约是“支持流”的fsvo。
就IReadOnlyList和IReadOnlyCollection而言,当存在“一个”隐含的稳定排序时,我使用前者,该排序 * 对索引有意义 *,而不考虑有目的的排序。例如,数组和列表可以作为IReadOnlyList返回,而HashSet最好作为IReadOnlyCollection返回。调用者总是可以根据需要将I[ReadOnly]List分配给I[ReadOnly]Collection:这个选择是关于暴露的“契约”,而不是一个程序员,“聪明”或其他人,会做什么。

ccrfmcuu

ccrfmcuu4#

看起来您可以返回一个 * 适当的接口 *:

...
    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<T>,因此IMHO的自然想法是公开最宽的接口,在本例中是IReadOnlyList<T>

slsn1g29

slsn1g295#

!! IEnumerable与IReadOnlyList的比较!!
IEnumerable从一开始就伴随着我们。多年来,它一直是表示只读集合的事实上的标准方法。然而,从. NET 4.5开始,有了另一种方法:I只读列表。
这两个集合接口都很有用。
〈〉

相关问题