返回Array和IList<>有什么区别?(关于Eric Lippert的有害数组)

8
我刚刚阅读了Eric Lippert的"Arrays considered somewhat harmful"文章。他告诉读者们,他们“可能不应该返回数组作为公共方法或属性的值”,并给出了以下理由(略有改动):
现在,调用者可以取出该数组并将其内容替换为任何他们想要的内容。合理的做法是返回 IList<>。你可以构建一个漂亮的只读集合对象,然后随意传递它的引用。
本质上,
一个数组是变量的集合。调用者不想要变量,但如果这是获取值的唯一方式,它们就会接受。但是,调用方和被调用方都不希望这些变量发生任何变化。
为了理解他所说的变量与值的区别,我创建了一个演示类,其中包含ArrayIList<>字段,并且有返回对两者引用的方法。这是在C# Pad上的链接。
class A {
    
    private readonly int[] arr = new []
    {
        10, 20, 30, 40, 50    
    };
    
    private readonly List<int> list = new List<int>(new []
    {
        10, 20, 30, 40, 50
    });
    
    public int[] GetArr()
    {
        return arr;
    }
    
    public IList<int> GetList()
    {
        return list;
    }
}

如果我理解正确,GetArr() 方法是Lippert不良实践,GetList() 是他的明智做法。以下是 GetArr() 的错误调用者可变性行为示例:
var a = new A();
var arr1 = a.GetArr();
var arr2 = a.GetArr();

Console.WriteLine("arr1[2]: " + arr1[2].ToString());  // > arr1[2]: 30
Console.WriteLine("arr2[2]: " + arr2[2].ToString());  // > arr2[2]: 30

// ONE CALLER MUTATES
arr1[2] = 99;

// BOTH CALLERS AFFECTED
Console.WriteLine("arr1[2]: " + arr1[2].ToString());  // > arr1[2]: 99
Console.WriteLine("arr2[2]: " + arr2[2].ToString());  // > arr2[2]: 99

但我不理解他的区分 - IList<> 引用也存在相同的调用者突变问题:
var a = new A();
var list1 = a.GetList();
var list2 = a.GetList();

Console.WriteLine("list1[2]: " + list1[2].ToString());  // > list1[2]: 30
Console.WriteLine("list2[2]: " + list2[2].ToString());  // > list2[2]: 30

// ONE CALLER MUTATES
list1[2] = 99;

// BOTH CALLERS AFFECTED
Console.WriteLine("list1[2]: " + list1[2].ToString());  // > list1[2]: 99
Console.WriteLine("list2[2]: " + list2[2].ToString());  // > list2[2]: 99

很明显,我相信Eric Lippert知道他在说什么,那么我哪里误解了他呢?以何种方式返回一个

2
我认为这里缺少的是遵循以下建议:“你可以构建一个不可变的集合对象,并且只需将其引用传递给所需的代码”。虽然,公平地说,现在返回数组也是合理的,但应该将返回值类型标记为‘IReadOnlyList<T>’。 - Dan Bryant
1
最常选择接口(在这种情况下是iList)的原因是其适应性(即,您可以以许多不同的方式满足给定的要求,而不像string[]那样只接受非常特定的格式);这就是这里要表达的观点。与数组本身无关(而是与其不太适应的本质有关)。在我看来(并且非常清楚的是,Eric Lippert对.NET语言的了解肯定比我更多),这种泛泛而谈的陈述很少是好的。数组在某些情况下是理想的方法,而iLists(甚至是Lists)在其他情况下则更合适。 - varocarbas
1
你的示例似乎表明,对于 IList<T>,你必须返回一个 List<T>,但你也可以返回数组,因为数组实现了 IList<T>;但是要小心,这种情况下不能使用 Add() 方法。 - Olivier Jacot-Descombes
1
@varocarbas 不,这确实与数组本身有关。无法使数组只读。因此,如果返回一个数组,则无法防止调用者修改该数组。因此,如果返回类似列表的结构,并将其作为数组返回,则不能安全地缓存数组以供后续调用使用。这就是博客文章的全部内容。因此,如果希望缓存列表或未来有任何真正可能成为缓存列表的机会,请勿返回数组。我不否认有例外情况,但如果您必须询问自己的情况是否属于其中之一,那么它可能不是。 - user743382
3
如果你打算传回一个 IList<T> 类型的只读集合,虽然从技术上讲这是正确的(因为我们有 IList<T>.IsReadOnly 标识),但我认为最好将其类型定义为 IReadOnlyList<T>(假设你使用的是 .NET 4.5 或更高版本)。原因是 IList<T> 通常被视为可变集合,并具有可变 API。很多时候,人们会认为如果他们拥有一个 IList<T>List<T>,那么它就可以改变。 - Chris Sinclair
显示剩余4条评论
3个回答

14

如果这篇文章是今天写的,几乎肯定会建议使用IReadOnlyList<T>,但是在2008年写这篇文章时,这种类型并不存在。唯一支持的具有只读索引列表接口的方法是使用IList<T>并简单地不实现突变操作。这样做确实可以让您返回一个调用者无法更改的对象,尽管从 API 方面来说并不一定清楚该对象是否是不可变的。自那时以后,在 .NET 中已经解决了这个问题。


这个帖子里有很多好的信息,但这段历史真正澄清了问题。 - kdbanman
如果这个答案是今天(2022年)写的,可能会建议使用ImmutableArray<T>IImmutableList<T>。 :-) - Theodor Zoulias
@TheodorZoulias 嗯,这是关于改变实现细节的讨论。公开暴露的 API 可能不希望成为这两者之一(即使有充分的理由将实际底层实现从可变列表/数组等更改为不可变数据结构,保持其作为“IReadOnlyList<T>”也是有意义的)。 - Servy
IImmutableList<T>是一个传达不可变性的接口,而IReadOnlyList<T>则不是。请参见为什么要使用ImmutableList而不是ReadOnlyCollection? - Theodor Zoulias
1
@TheodorZoulias 嗯,那很公平。 - Servy
显示剩余2条评论

3

List<T>对象作为IList<T>返回并不是他推荐的做法。相反,你应该创建一个只读对象来实现IList<T>并返回它。例如,你可以使用ReadOnlyCollection<T>对象。


“将 List<T> 对象作为 IList<T> 返回并不是他所推荐的。” 我认为你是对的。我想我只是被 IList<> 接口表达的可变性所迷惑了。如果他建议直接返回一个 IReadOnlyList<>,一切都会更清晰明了。 - kdbanman
2
@kdbanman 在博客文章发布时,IReadOnlyList<> 还不存在。如果他今天写这篇文章,我相信会使用 IReadOnlyList<>。 :) - user743382

2
他所说的是能够使用只读的具体类型实现IList<T>的能力:
如果您正在编写这样的API,请使用ReadOnlyCollection将数组包装起来,并返回IEnumerable或IList或其他内容,但不要返回数组。(当然,不要简单地将数组转换为IEnumerable并认为自己完成了!那仍然是传递变量;调用者可以简单地将其转换回数组!只有在受到只读对象的包装时才传递出数组。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接