68

扩展方法ToList()返回一个List<TSource>. 遵循相同的模式,ToDictionary()返回 a Dictionary<TKey, TSource>

我很好奇为什么这些方法不分别输入它们的返回IList<TSource>IDictionary<TKey, TSource>。这似乎更奇怪,因为ToLookup<TSource, TKey>它的返回值类型是接口而不是实际实现。

使用dotPeek或其他反编译器查看这些扩展方法的来源,我们看到以下实现(显示ToList()是因为它更短):

public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source) { 
   if (source == null) throw Error.ArgumentNull("source");
   return new List<TSource>(source); 
}

那么为什么这个方法将它的返回值作为接口的具体实现而不是接口本身呢?唯一的变化是返回类型。

我很好奇,因为IEnumerable<>除了这两种情况外,扩展名的签名非常一致。我一直觉得这有点奇怪。

此外,为了让事情变得更加混乱,状态文档ToLookup()

根据指定的键选择器函数从 IEnumerable 创建一个 Lookup。

但返回类型是ILookup<TKey, TElement>.

Edulinq中,Jon Skeet 提到返回类型是List<T>而不是IList<T>,但没有进一步触及主题。
广泛搜索没有得到任何答案,所以我在这里问你:

在不将返回值输入为接口的背后是否有任何设计决定,还是只是偶然?

4

10 回答 10

50

ReturningList<T>的优点是那些List<T>不属于其中的IList<T>方法很容易使用。有很多事情你可以用 a 做List<T>而你不能用 a 做IList<T>

相反,Lookup<TKey, TElement>只有一种ILookup<TKey, TElement>没有 ( ApplyResultSelector) 的可用方法,而且您可能最终也不会使用它。

于 2013-02-07T14:23:37.707 回答
12

这类决定可能让人觉得很随意,但我猜是ToList()返回List<T>而不是接口,因为List<T>两者都实现了,但它添加了常规类型对象IList<T>中不存在的其他成员。IList<T>

例如,AddRange()

看看IList<T>应该实现什么(http://msdn.microsoft.com/en-us/library/5y536ey6.aspx):

public interface IList<T> : ICollection<T>, 
    IEnumerable<T>, IEnumerable

并且List<T>http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx):

public class List<T> : IList<T>, ICollection<T>, 
    IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, 
    IEnumerable

也许您自己的代码不需要IReadOnlyList<T>,IReadOnlyCollection<T>ICollection,但 .NET Framework 和其他产品上的其他组件可能依赖于更专业的列表对象,这就是 .NET 开发团队决定不返回接口的原因。

不要觉得总是返回一个接口是最好的做法。如果您的代码或第三方代码需要这种封装。

于 2013-02-07T14:29:35.860 回答
8

仅仅拥有List一个IList. 首先,ListIList没有的方法。您还知道实现是什么,这使您可以推断它的行为方式。您知道它可以有效地添加到结尾,但不能添加到开头,您知道它的索引器非常快,等等。

您无需担心结构被更改为 aLinkedList并破坏应用程序的性能。当涉及到这样的数据结构时,在很多情况下了解数据结构的实现方式确实很重要而不仅仅是它遵循的合同。这是不应该改变的行为。

您也不能将 an 传递IList给接受 a 的方法List,这是您经常看到的。 ToList经常使用,因为该人确实需要 , 的实例List来匹配他们无法控制的签名,并且IList对此无济于事。

然后我们问自己回归有什么好处IList。好吧,我们可能会返回列表的其他一些实现,但如前所述,这可能非常有害后果,几乎可以肯定比使用任何其他类型可能获得的后果要多得多。使用接口而不是实现可能会给您带来温暖的模糊感,但即使在这种情况下,我也不认为这是一种好的心态(通常或)。通常,返回一个接口通常不如返回一个具体实现。“在你接受的东西上要自由,在你提供的东西上要具体。” 在可能的情况下,您的方法的参数应该是定义您需要的最少功能的接口,您的调用者可以传入任何您需要的实现,并且您应该提供与调用者一样具体的实现“允许”查看,以便他们可以尽可能多地处理该对象的结果。

所以现在我们继续前进,“为什么要返回ILookup而不是返回Lookup?” 嗯,首先Lookup不是公开课。Lookup中没有System.Collections.*。通过 LINQ 公开的Lookup类不公开公开构造函数。除了 through 之外,您无法使用该类ToLookup。它还没有公开尚未通过ILookup. 在这种特殊情况下,他们专门围绕这个确切的方法 ( ToLookup) 设计了接口,并且Lookup该类是专门为实现该接口而设计的类。由于所有这些,几乎所有讨论的要点List都不适用于这里。退货会不会有问题Lookup相反,不,不是。在这种情况下,这两种方式都无关紧要。

于 2013-02-07T15:14:28.267 回答
6

在我看来,返回 aList<T>的理由是方法名称说ToList. 否则它必须被命名ToIListIEnumerable<T>将非特定类型转换为特定类型正是此方法的目的List<T>

如果您有一个名称不明的方法,例如GetResults,那么返回类型类似于IList<T>orIEnumerable<T>对我来说似乎很合适。


如果您查看Lookup<TKey, TElement>使用反射器的类的实现,您会看到很多internal成员,这些成员只能由 LINQ 本身访问。没有公共构造函数,Lookup对象是不可变的。因此,Lookup直接曝光没有任何优势。

Lookup<TKey, TElement>类似乎是一种 LINQ 内部的,不适合公共使用。

于 2013-02-07T14:31:38.990 回答
4

我相信返回 List<> 而不是 IList<> 的决定是调用 ToList 的更常见用例之一是强制立即评估整个列表。通过返回List <> 可以保证这一点。使用IList <> 实现仍然可以是惰性的,这会破坏调用的“主要”目的。

于 2013-02-07T17:34:34.287 回答
3

通常,当您在某个方法上调用 ToList() 时,您正在寻找一个具体类型,否则该项目可能会保留为 IEnumerable 类型。除非您正在做一些需要具体列表的事情,否则您不需要转换为列表。

于 2013-02-07T14:17:45.320 回答
3

这是程序员在使用接口和具体类型时难以理解的常见问题之一。

返回一个具体List<T>的实现IList<T>只会为方法使用者提供更多信息。这是 List 对象实现的(通过 MSDN):

[SerializableAttribute]
public class List<T> : IList<T>, ICollection<T>, IList, ICollection, 
    IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable

作为 a 返回List<T>使我们能够在所有这些接口上调用成员,除了List<T>它本身。例如,我们只能List.BinarySearch(T)在 a 上使用List<T>,因为它存在于List<T>但不存在于 中IList<T>

一般来说,为了最大限度地提高我们方法的灵活性,我们应该将最抽象的类型作为参数(即仅我们将要使用的东西)并返回尽可能少的抽象类型(以允许更多功能的返回对象)。

于 2013-02-08T02:37:08.347 回答
2

因为List<T>实际上实现了一系列接口,而不仅仅是 IList:

public class List<T> : IList<T>, ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable{

}

这些接口中的每一个都定义了列表必须符合的一系列特性。选择一个特定的,将使大部分实现无法使用。

如果您确实想返回 IList,没有什么能阻止您拥有自己的简单包装器:

public static IList<TSource> ToIList<TSource>(this IEnumerable<TSource> source)
{
    if (source == null) throw new ArgumentNullException(source);
    return source.ToList();
}
于 2013-02-07T20:18:18.997 回答
2

简短的回答是,权威的框架设计指南通常建议返回最具体的可用类型。(抱歉,我手头没有引用,但我清楚地记得这一点,因为它与 Java 社区指南相反,后者更喜欢相反)。

这对我来说很有意义。例如,您总是可以这样做IList<int> list = x.ToList(),只有库作者需要关心是否能够支持具体的返回类型。

ToLookup<T>是人群中独一无二的。但完全符合准则:它库作者愿意支持的最具体的可用类型(正如其他人所指出的,具体Lookup<T>类型似乎更像是一种不供公众使用的内部类型)。

于 2013-02-07T21:34:18.800 回答
1

如果一个函数返回一个新构造的不可变对象,调用者通常不应该关心返回的精确类型,只要它能够保存它包含的实际数据。例如,一个应该返回 an 的函数IImmutableMatrix通常可能返回ImmutableArrayMatrix由私有数组支持的 an ,但如果所有单元格都保存零,它可能会返回一个ZeroMatrix,仅由WidthandHeight字段支持(使用一个简单地返回零的 getter时间)。调用者不会关心它是否被赋予了一个ImmutableArrayMatrix矩阵或一个ZeroMatrix; 这两种类型都将允许读取它们的所有单元格,并保证它们的值永远不会改变,这就是调用者所关心的。

另一方面,返回允许开放式突变的新构造对象的函数通常应该返回调用者期望的精确类型。除非有一种方法可以让调用者请求不同的返回类型(例如通过调用ToyotaFactory.Build("Corolla")vs ToyotaFactory.Build("Prius")),否则声明的返回类型没有理由是其他任何东西。虽然返回不可变数据保存对象的工厂可以根据要包含的数据选择类型,但返回自由可变类型的工厂将无法知道可以放入哪些数据。如果不同的调用者有不同的需求(例如,回到现存的例子,一些调用者的需求可以通过数组来满足,而另一些则不会)他们应该可以选择工厂方法。

顺便说一句,类似的东西IEnumerator<T>.GetEnumerator()有点特殊。返回的对象几乎总是可变的,但仅限于非常严格的方式;实际上,期望返回的对象,无论其类型如何,都将只有一个可变状态:它在枚举序列中的位置。尽管IEnumerator<T>预期 an 是可变的,但在派生类实现中会发生变化的状态部分却不是。

于 2013-02-07T15:50:10.463 回答