2

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

最近一直在考虑回国的利弊 IEnumerable<T>

从好的方面来说,它几乎与接口一样小,因此它为方法作者提供了比提交更重的替代方案(如IList<T>或(天堂禁止)数组)更大的灵活性。

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

需要一种更好的方法来锁定退回的收藏品,而不会使这种诱惑如此突出。(我想起了巴尼·法夫(Barney Fife)艰难地学习这一课。)

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

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

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

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

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

Microsoft 指南特别指出:

一般来说,更喜欢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.
    }
}

所以用一个例子来说明这个困境,如果我们采用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)?

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

4

1 回答 1

6

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

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

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

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

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

至于使用IList<T>vs IReadOnlyList<T>or IReadOnlyCollection<T>,只要您可以始终返回列表的副本,这应该很好。如果您有一个保存在内存中的列表,您不希望调用者修改它。

于 2018-03-14T15:37:06.430 回答