为什么IEnumerable<;T>;.ToList<;T>;()return List<;T>

本文关键字:gt lt return List ToList 为什么 IEnumerable | 更新日期: 2023-09-27 17:58:23

扩展方法ToList()返回一个List<TSource>。按照相同的模式,ToDictionary()返回一个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()的文档中指出:

根据指定的键选择器功能。

但返回类型为CCD_ 11。

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

在不将返回值键入为接口的背后是否有任何设计决策,或者这只是偶然事件

为什么IEnumerable<;T>;.ToList<;T>;()return List<;T>

返回List<T>的优点是List<T>的那些不属于IList<T>的方法很容易使用。使用List<T>可以做很多事情,而使用IList<T>却做不到。

相比之下,Lookup<TKey, TElement>只有一个ILookup<TKey, TElement>没有的可用方法(ApplyResultSelector),您可能无论如何都不会使用它。

这类决策可能会让人觉得是任意的,但我猜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开发团队决定不返回接口的原因。

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

IList相比,仅仅拥有List有很多优点。首先,List具有IList没有的方法。您还知道实现是什么,这使您能够推断它的行为。你知道它可以有效地添加到末尾,但不能添加到开头,你知道它的索引器非常快,等等

您不需要担心您的结构被更改为LinkedList并破坏应用程序的性能。当涉及到这样的数据结构时,在很多情况下,了解您的数据结构是如何实现的,而不仅仅是它所遵循的合同,这一点非常重要。这种行为永远不应该改变。

您也不能将IList传递给接受List的方法,这是您经常看到的情况。ToList经常被使用,因为人们确实需要List的实例来匹配他们无法控制的签名,而IList对此没有帮助。

然后我们问自己,返回IList有什么好处。好吧,我们可能会返回列表的其他实现,但如前所述,这可能会产生非常的有害后果,几乎可以肯定,这比使用任何其他类型都可能获得的结果要多得多。使用接口而不是实现可能会给你带来温暖的模糊感,但即使这样,我觉得在这种情况下(一般来说或)也不是一种好的心态。作为一条规则,返回接口通常不比返回具体实现更可取。"在接受什么方面要自由,在提供什么方面要具体。"在可能的情况下,方法的参数应该是定义你需要的最少功能的接口,你的调用者可以在任何实现中传递你需要的功能,您应该提供调用方"允许"看到的具体实现,这样他们就可以尽可能多地处理该对象所能处理的结果。传递一个接口会限制这个值,这只是偶尔你想做的事情

现在我们继续讨论,"为什么返回ILookup而不是Lookup?"首先,Lookup不是公共类。System.Collections.*中没有Lookup。通过LINQ公开的Lookup类没有公开任何构造函数。除非通过ToLookup,否则您不能使用该类。它还没有公开任何尚未通过ILookup公开的功能。在这种特殊的情况下,他们专门围绕这个精确的方法(ToLookup)设计了接口,而Lookup类是专门为实现该接口而设计的类。正因为如此,几乎所有关于List的讨论都不适用于此。返回Lookup会不会是一个问题,不,不是真的。在这种情况下,无论哪种方式,它都不重要。

在我看来,返回List<T>是合理的,因为方法名称显示ToList。否则,它将不得不命名为ToIList。该方法的目的正是将不特定的IEnumerable<T>转换为特定类型的List<T>

如果你有一个像GetResults这样名称不明确的方法,那么像IList<T>IEnumerable<T>这样的返回类型对我来说似乎很合适


如果您查看带有反射器的Lookup<TKey, TElement>类的实现,您将看到许多internal成员,它们只能由LINQ本身访问。没有公共构造函数,Lookup对象是不可变的。因此,直接暴露CCD_ 67没有任何好处。

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

我认为返回列表的决定<>而不是IList<>调用ToList的一个更常见的用例是强制立即评估整个列表。通过返回列表<>这是有保证的。带有IList<>实现仍然可能是惰性的,这将破坏调用的"主要"目的。

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

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

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

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

通常,为了最大限度地提高方法的灵活性,我们应该将最抽象的类型作为参数(即,只使用我们要使用的东西),并返回尽可能少的抽象类型

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

简单的答案是,权威的框架设计指南通常建议返回最具体的可用类型。(很抱歉,我手头没有引文,但我清楚地记得这一点,因为它与Java社区指南形成了鲜明对比,后者更倾向于相反)。

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

ToLookup<T>是人群中唯一的一个。但完全在指南范围内:它是图书馆作者愿意支持的最具体的类型(正如其他人所指出的,具体的Lookup<T>类型似乎更像是一种不适合公众使用的内部类型)。

因为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();
}

如果函数返回一个新构建的不可变对象,调用方通常不应该关心返回的精确类型,只要它能够保存它所包含的实际数据。例如,一个本应返回IImmutableMatrix的函数通常可能返回一个由私有数组支持的ImmutableArrayMatrix,但如果所有单元格都保持零,它可能会返回一个ZeroMatrix,仅由WidthHeight字段支持(使用一个始终只返回零的getter)。呼叫者不会关心它被给予的是ImmutableArrayMatrix矩阵还是ZeroMatrix;这两种类型都将允许读取它们的所有单元格,并保证它们的值永远不会改变,而这正是调用者所关心的。

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

顺便说一句,像IEnumerator<T>.GetEnumerator()这样的东西有点特殊。返回的对象几乎总是可变的,但只能以高度受限的方式;事实上,不管返回的对象的类型如何,它都应该具有一个可变状态:它在枚举序列中的位置。尽管IEnumerator<T>被认为是可变的,但它的状态在派生类实现中会发生变化的部分却不是。