使用 ReadOnlyCollection<T> vs List<T> vs array 作为返回类型

Posted

技术标签:

【中文标题】使用 ReadOnlyCollection<T> vs List<T> vs array 作为返回类型【英文标题】:Using ReadOnlyCollection<T> vs List<T> vs array for return types 【发布时间】:2018-08-23 05:06:32 【问题描述】:

我前段时间发布了一个similar question关于参数,它提到an article与这个问题更相关,它主张使用IReadOnlyCollection&lt;T&gt;而不是IEnumerable&lt;T&gt;

最近一直在考虑回国的利弊 IEnumerable&lt;T&gt;.

从好的方面来说,它几乎是一个界面所获得的最小限度,所以它 让您作为方法作者比承诺更灵活 较重的替代方案,例如 IList&lt;T&gt; 或 (heaven forbid) 数组。

但是,正如我在 the last post 中概述的那样,IEnumerable&lt;T&gt; 返回 引诱来电者违反Liskov Substitution Principle。它太 他们很容易使用Last()Count()等LINQ扩展方法, 其语义IEnumerable&lt;T&gt; 不承诺。

需要一种更好的方法来锁定返回的集合 没有使这种诱惑如此突出。 (我想起了巴尼 法夫艰难地学习了这一课。)

输入IReadOnlyCollection&lt;T&gt;,.NET 4.5 中的新功能。它只增加了一个 IEnumerable&lt;T&gt; 的属性:Count 属性。通过承诺计数, 你向你的来电者保证你的IEnumerable&lt;T&gt; 确实有一个 终点站。然后他们可以使用 LINQ 扩展方法,例如 Last() 无愧于心。

但是,this article 主张在您想要保护成员的情况下使用 ReadOnlyCollection&lt;T&gt;(和其他替代方法)。

所以,我和同事讨论过这个问题,我们有不同的意见。

他建议一个方法应该返回它所拥有的任何东西(例如 List&lt;T&gt; 或数组),如果该方法生成它并且调用者之后更改它不会对其他任何东西产生影响(即,它不是成员并且不'不属于任何生命周期大于方法调用的东西)。

我的观点是ReadOnlyCollection&lt;T&gt; 向调用者表明集合通常应该保持原样。如果他们想更改它,那么他们有责任将其显式转换为IList&lt;T&gt; 或调用ToList

Microsoft guidelines 的具体状态:

一般来说,更喜欢ReadOnlyCollection&lt;T&gt;

但除了声明使用Collection&lt;T&gt; 来返回读/写集合之外,它似乎没有定义一般情况。但是我的同事是否认为消费者可能想要添加到集合中并且我们不在乎他们是否定义了返回读/写集合?

编辑

在回答一些回复时,为了尝试为我的问题添加更多上下文,我在下面放置了一些代码来演示与我的问题相关的 cmets 的各种场景:

class Foo

    private readonly int[] array =  1 ;
    private readonly List<int> list = new List<int>(new[] 1);

    // Get member array.
    public int[] GetMemberArrayAsIs() => array;

    public IEnumerable<int> GetMemberArrayAsEnumerable() => array;

    public ReadOnlyCollection<int> GetMemberArrayAsReadOnlyCollection() => new ReadOnlyCollection<int>(array);

    // Get local array.
    public int[] GetLocalArrayAsIs() => new[]  1 ;

    public IEnumerable<int> GetLocalArrayAsEnumerable() => new[]  1 ;

    public ReadOnlyCollection<int> GetLocalArrayAsReadOnlyCollection() => new ReadOnlyCollection<int>(new[]  1 );

    // Get member list.
    public Collection<int> GetMemberListAsIs() => new Collection<int>(list);

    public IEnumerable<int> GetMemberListAsEnumerable() => array;

    public ReadOnlyCollection<int> GetMemberListAsReadOnlyCollection() => new ReadOnlyCollection<int>(array);

    // Get local list.
    public Collection<int> GetLocalListAsIs() => new Collection<int>(new[]  1 );

    public IEnumerable<int> GetLocalListAsEnumerable() => new List<int>(new[]  1 );

    public ReadOnlyCollection<int> GetLocalListAsReadOnlyCollection() => new List<int>(new[]  1 ).AsReadOnly();



class FooTest

    void Test()
    
        var foo = new Foo();
        int count;

        // Get member array.
        var array1 = foo.GetMemberArrayAsIs(); // ReSharper encourages to make the return type IEnumerable<T>.
        count = array1.Length;  // ...unless we do this.

        var enumerable1 = foo.GetMemberArrayAsEnumerable();
        enumerable1.Concat(enumerable1); // Warning of possible multiple enumeration.

        var roc1 = foo.GetMemberArrayAsReadOnlyCollection(); // ReSharper encourages to make the return type IEnumerable<T>.
        count = roc1.Count;  // ...unless we do this.


        // Get local array.
        var array2 = foo.GetLocalArrayAsIs(); // ReSharper encourages to make the return type IEnumerable<T>.
        count = array2.Length;  // ...unless we do this.

        var enumerable2 = foo.GetLocalArrayAsEnumerable();
        enumerable2.Concat(enumerable2); // Warning of possible multiple enumeration.

        var roc2 = foo.GetLocalArrayAsReadOnlyCollection(); // ReSharper encourages to make the return type IEnumerable<T>.
        count = roc2.Count;  // ...unless we do this.


        // Get member list.
        var list1 = foo.GetMemberListAsIs(); // ReSharper encourages to make the return type IEnumerable<T>.
        count = list1.Count;  // ...unless we do this.
        list1.Add(2); // This affects the Foo object as the collection is a member. DANGEROUS!

        var enumerable3 = foo.GetMemberListAsEnumerable();
        enumerable3.Concat(enumerable3); // Warning of possible multiple enumeration.

        var roc3 = foo.GetMemberListAsReadOnlyCollection(); // ReSharper encourages to make the return type IEnumerable<T>.
        count = roc3.Count;  // ...unless we do this.


        // Get local list.
        var list2 = foo.GetLocalListAsIs(); // ReSharper encourages to make the return type IEnumerable<T>.
        count = list2.Count;  // ...unless we do this.
        list2.Add(2); // This doesn't affect the Foo object as the collection was produced by the method.

        var enumerable4 = foo.GetLocalListAsEnumerable();
        enumerable4.Concat(enumerable4); // Warning of possible multiple enumeration.

        var roc4 = foo.GetLocalListAsReadOnlyCollection(); // ReSharper encourages to make the return type IEnumerable<T>.
        count = roc4.Count;  // ...unless we do this.
    

所以用一个例子来说明这个困境,如果我们取GetMemberArrayAsIs,如果调用代码只枚举返回值一次,ReSharper 告诉我将返回类型更改为IEnumerable&lt;T&gt;。如果我多次枚举返回的值,那么我必须保持原样以避免可能的多次枚举警告。但是,这就是我的问题所在。我是否应该返回 ReadOnlyCollection&lt;T&gt; 以符合我对 Microsoft 指南的理解,而不是返回数组?这需要我实例化一个ReadOnlyCollection&lt;T&gt;,如GetMemberArrayAsReadOnlyCollection 所示。或者,我们是否应该像我的同事认为的那样返回数组 (GetMemberArrayAsIs)?

或者,我可以坚持返回 IEnumerable&lt;T&gt; 并让调用者有责任在多次使用该集合之前进行枚举,但我认为 Microsoft 偏爱使用 ReadOnlyCollection&lt;T&gt; 是为了避免这种情况。

List&lt;T&gt; 的情况下,困境会稍微复杂一些,因为它有可能被更改,如果它是成员,这是一个问题。在这种情况下,我当然应该返回一个ReadOnlyCollection&lt;T&gt; (GetMemberListAsReadOnlyCollection)。但是对于本地列表,我是否应该返回 ReadOnlyCollection&lt;T&gt; 以符合我对 Microsoft 指南的理解?这需要我实例化一个ReadOnlyCollection&lt;T&gt;,如GetLocalListAsReadOnlyCollection 所示。或者,我们是否应该像我的同事认为的那样返回列表 (GetMemberListAsIs)?

我希望这可以为这个问题增加更多背景,并且在某些方面它是相当哲学的。但其中很多是关于应该如何解释微软指南,以及是否应该让提供者有责任转换为ReadOnlyCollection&lt;T&gt;,或者让消费者在多次使用之前枚举返回值(使用ToArray ),或者实际上两者都没有并按原样返回。

【问题讨论】:

太模糊 - 从什么返回 ?您应该如何编写代码取决于上下文、预期用户、范围等,因此任何单一规则都比无用更糟糕。 @buffjape 好的,我在问题中添加了更多信息,包括代码。 【参考方案1】:

这个问题可能会被关闭,因为它含糊不清并且没有一个正确的答案,但我还是会尝试。

我想说,如果您返回一个对象是 List&lt;T&gt;T[],您可能希望将其公开为至少 IReadOnlyCollection&lt;T&gt; 或更好的 IReadOnlyList&lt;T&gt;,它具有索引器支持。这样做的好处是:

调用ToList 不需要对同一输出进行多次枚举。 界限分明 使用IReadOnlyList&lt;T&gt;,您可以使用for 循环而不是foreach 进行枚举,这在某些极端情况下可能是有利的。

我看到的主要缺点,根据实施情况可能是一个很大的缺点:

您的 API 将不再能够以流式方式返回 IEnumerable&lt;T&gt;(例如从 StreamIDbDataReader 读取等)。这意味着在您的代码中引入重大更改。

至于使用IList&lt;T&gt;IReadOnlyList&lt;T&gt;IReadOnlyCollection&lt;T&gt;,这应该是完全可以的,只要您可以始终返回列表的副本。如果您有一个保存在内存中的列表,您不会希望调用者修改它。

【讨论】:

感谢您的回答。我已经用上下文和一些代码更新了这个问题,使它更清楚。也许你现在可以修改你的答案?

以上是关于使用 ReadOnlyCollection<T> vs List<T> vs array 作为返回类型的主要内容,如果未能解决你的问题,请参考以下文章

ReadonlyCollection,对象是不可变的吗?

将 ReadOnlyCollection<byte> 写入流

使用 ReadOnlyCollection<T> vs List<T> vs array 作为返回类型

C# 将 ReadOnlyCollection<int> 转换为 byte[] 数组

将 Dictionary<TKey, List<TValue>> 转换为 ReadOnlyDictionary<TKey, ReadOnlyCollection<T

迭代时修改 Collection 底层的 ReadOnlyCollection 属性