对<T>返回类型使用只读集合、列表<T>和数组



我不久前发布了一个关于参数的类似问题,它提到了一篇与这个问题更相关的文章,该文章主张使用IReadOnlyCollection<T>而不是IEnumerable<T>

最近,我一直在考虑回归的优缺点IEnumerable<T>.

从好的方面来说,它与界面一样小,因此它 作为方法作者,您比提交 更重的替代品,如IList<T>或(上帝保佑)阵列。

但是,正如我在上一篇文章中所概述的那样,IEnumerable<T>回报 引诱来电者违反利斯科夫替代原则。太了 他们很容易使用LINQ扩展方法,如Last()Count(), 谁的语义IEnumerable<T>不承诺。

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

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

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

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

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

我的观点是,ReadOnlyCollection<T>向调用方指示集合通常应保持原样。如果他们想改变它,那么他们有责任将其显式投射到IList<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准则的理解,而不是返回数组? 这将需要我实例化ReadOnlyCollection<T>,如GetMemberArrayAsReadOnlyCollection所示。 或者,我们是否应该像我的同事认为的那样返回数组(GetMemberArrayAsIs)?

或者,我可以坚持返回IEnumerable<T>,并在多次使用集合之前将枚举的责任放在调用者身上,但我认为Microsoft更喜欢使用ReadOnlyCollection<T>来避免这种情况。

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

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

这个问题可能会被关闭,因为它很模糊,没有单一的正确答案,但我还是会尝试。

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

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

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

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

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

最新更新