使用 ReadOnlyCollection<T> 与 List<T> 与数组作为返回类型

Neo*_*Neo 1 c# arrays list return-type readonly-collection

前段时间我发了一个关于参数的类似问题,它引用了一篇这个问题更相关的文章该文章提倡使用IReadOnlyCollection<T>over IEnumerable<T>

最近,我一直在考虑回归的利弊 IEnumerable<T>

从好的方面来说,它与接口一样小,因此它使您作为方法作者比承诺更重的替代方案(如IList<T>或(天堂禁止)数组)具有更大的灵活性。

然而,正如我在一篇文章中概述的那样,IEnumerable<T>返回会诱使调用者违反Liskov 替换原则。这太容易让他们使用LINQ扩展方法,如Last()Count(),其语义IEnumerable<T>不答应。

需要的是一种更好的方法来锁定返回的集合,而不会使这种诱惑如此突出。(我想起了 Barney Fife 以艰难的方式学习这一课。)

输入IReadOnlyCollection<T>, .NET 4.5 中的新内容。它只向IEnumerable<T>:Count属性添加了一个属性。通过承诺计数,您可以向呼叫者保证您IEnumerable<T>确实有终点站。然后他们可以Last()问心无愧地使用 LINQ 扩展方法。

但是,本文提倡ReadOnlyCollection<T>在您想要保护成员的情况下使用(和其他替代方法)。

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

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

我的观点是ReadOnlyCollection<T>向调用者表明集合通常应保持原样。如果他们想改变它,那么他们有责任明确地将它转换为 aIList<T>或调用ToList它。

微软的准则在特定的状态:

一般来说,更喜欢ReadOnlyCollection<T>

但除了声明Collection<T>用于返回读/写集合之外,它似乎并没有定义一般情况。但是我的同事是否认为消费者可能想要添加到集合中,而我们不关心他们是否定义了返回读/写集合的定义?

编辑

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

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.
    }
}
Run Code Online (Sandbox Code Playgroud)

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

或者,我可以坚持在IEnumerable<T>多次使用集合之前返回并让调用者有责任进行枚举,但我认为微软更喜欢使用它ReadOnlyCollection<T>是为了避免这种情况。

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

我希望这为这个问题增加了更多的背景,并且在某些方面它是相当哲学的。但是很多都是关于应该如何解释 Microsoft 指南,以及是否应该由提供者承担转换为 的责任ReadOnlyCollection<T>,或者消费者在多次使用之前枚举返回的值(使用ToArray),或者实际上两者都不和原样返回。

Bas*_*Bas 6

这个问题可能会被关闭,因为它很模糊并且没有单一的正确答案,但无论如何我都会尝试。

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

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

我看到的主要缺点,这可能是一个很大的缺点,具体取决于实现:

  • 您的 API 将不再能够以IEnumerable<T>流式方式返回 an (例如从 aStream或读取IDbDataReader等)。这意味着在您的代码中引入重大更改。

至于使用IList<T>vsIReadOnlyList<T>IReadOnlyCollection<T>,只要你总是返回列表的副本就可以了。如果您有一个保存在内存中的列表,您不希望调用者修改它。