为什么IEnumerable <T> .ToList <T>()返回List <T>而不是IList <T>?


67

扩展方法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()

根据指定的键选择器函数从IEnumerable创建查找。

但返回类型为ILookup<TKey, TElement>

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

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


3
List <T>实现IList <T>。您无法实例化接口,而必须实例化某些东西。即使IEnumerable <T>确实是后台中的其他东西,您也只是使用接口来访问它。但是接口不是一个完整的对象。
sircodesalot

15
@sircodesalot Waldfee清楚地意识到了这一点。他的整个问题都基于这一事实!
RB。

2
@sircodesalot:的确如此。如果不是这样,那么这个问题就没有任何意义。
丹尼尔·希尔加斯

3
我想没关系-因为它必须将列表包装为具体类型并将某些内容返回给调用者,所以它最好也返回列表,因为IList这是使用的特定实现。只要它返回一个IList实现,它对您来说就不成问题。公平地说,该方法也称为“ToList不” ToIList
-Charleh

4
我不确定在没有.Net设计团队的评论的情况下能否获得明确的答案,但我们可以认为这是一个务实的选择。该List班是更多特色与比更丰富的扩展IList接口。在实用性和纯度之间做出选择。
Jodrell 2013年

Answers:


49

返回List<T>的优点是可以轻松使用那些方法的List<T>一部分IList<T>。有很多事情可以做,List<T>而不能做IList<T>

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


7
这可能是答案,但是,如果不记录设计过程中所做的努力,我们怎么知道。(+1)
乔德雷尔

2
同意,问题开始于“为什么做”,而不是“为什么做”。我相信没有人会把我的答案误解为基本原理而不是基本原理。:)

4
+1为正确答案。然而,微软应该真正采取这些方法具体到List<T>和他们更改为操作上的扩展方法IList<T>-这样一来,所有 IList<T>的实现可能受益于Reverse()Sort()等等,而不仅仅是List<T>!几年前,我在MS Connect上提出了这个建议。它得到了很多好评,他们说这是一个好主意,但一直将其推向下一版本。然后他们完全删除了该建议:(
BlueRaja-Danny Pflughoeft13年

@ BlueRaja-DannyPflughoeft:添加此类扩展方法的一个主要问题是,不清楚语义应该是什么。应该调用其中一个从集合中删除所有项目并以正确的顺序添加它们,还是应该删除每个项目并在适当的位置重新插入它们,或者是什么?我希望这些东西已经包括在内,IList<T>但是框架提供了实现者可以链接到的方法,以及更好的方法来查询集合可以支持的操作以及它可以保证的可变性或不可变性程度。
超级猫

@supercat我在这里没有问题-etc的实现Reverse()应该是他们所记录的那样。没有理由不就地执行此操作,因此我想他们会这样做。另外,我的建议是将这些扩展方法添加到IList<T>,而不是更广泛地添加ICollection<T>-通过实现IList<T>,实现者已经同意一定程度的可变性。
BlueRaja-Danny Pflughoeft13年

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/zh-cn/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开发团队决定不返回接口。

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


非常清楚的解释,谢谢。List除了之外IList,我很少使用功能实现,因此没有想到。
Waldfee 2013年

至于最后一段,返回接口或具体类型是一种情况决定,而不是一个适合所有情况的决定,我的问题专门针对这种情况。
Waldfee 2013年

1
@Waldfee没问题。认为.NET FW是很多事情的基础。也许某些类型和决策并没有反映在框架本身中,而是反映在其他产品的需求甚至用户故事中。我不是来自.NET devteam,但这应该是原因。关于最后一段,当然,我只是想指出,返回接口是不总是要求:)
马蒂亚斯Fidemraizer

问题不是IList<T>“应该”实施什么,而是实际上包括什么。有很多事情应该存在,IList<T>但不是。即使像这样的事情SortAddRange可以通过处理IList<T>单个项目的静态帮助器方法来处理此类事情,并且许多实现可能会链接到此类方法,但在许多情况下,对性能敏感的实现可能会取得优于2倍的效果(如果没有,通过直接在其后备店实施此类操作来提高10倍的速度。
2013年

@supercat我的回答是,讨论的是,这儿有例外的规则,如果大量的代码取决于使用List<T>,为什么要强迫总是垂头丧气不仅是因为其余方法进行回归IList<T>。这应该是原因,实际上,这就是我用自己的代码进行决策的方式。没有一种解决所有问题的模式。我同意你的看法:也许某些List<T>特定的方法应该作为扩​​展方法来实现。并且 IList<T>应该也能做到AddRange
马蒂亚斯Fidemraizer

8

仅需输入一个List以上的字就有很多优点IList。首先,ListIList没有的方法。你也知道实现是什么,它使您能够推理其行为。您知道它可以有效地添加到末尾,但不能有效地添加到开头,您知道它的索引器非常快,等等。

您不必担心将结构更改为aLinkedList并破坏应用程序的性能。当涉及到这样的数据结构时,在很多情况下了解如何你的数据结构来实现,而不仅仅是合同,它遵循。这种行为永远都不会改变。

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

然后我们问自己,回归有什么好处IList。好吧,我们可能会返回列表的其他实现,但是正如之前提到的那样,有害的后果,几乎可以肯定比使用其他任何类型的后果要多得多。使用接口而不是实现可能会让您感到困惑,但是就此而言,即使是那样,我也不认为这是一种良好的心态。通常,返回接口通常不如返回具体实现。“在接受的东西上要放宽,在提供的东西上要专一。” 方法的参数在可能的情况下应该是定义调用程序可以在需要实现其功能的任何实现中传递的所需最少功能的接口,并且应提供与调用程序一样具体的实现“允许”查看,以便他们可以执行与该对象能够执行的结果一样的工作。

因此,现在我们继续前进,“为什么ILookup不回来Lookup?” 好吧,首先Lookup不是公开课。有没有LookupSystem.Collections.*。该Lookup所通过LINQ暴露类公开不公开的构造。你不能够使用类,除非通过ToLookup。它还没有公开尚未通过公开的功能ILookup。在这种特殊情况下,他们专门围绕此确切方法(ToLookup)设计了接口,并且Lookup该类是专门设计用于实现该接口的类。由于所有这些,因此实际上讨论的所有要点List都不适用于此处。会不会是个问题退货Lookup相反,不是,不是真的。在这种情况下,无论哪种方式,它实际上都没有多大关系。


好的和彻底的答案,尽管我不得不不同意最后一段的第一部分:System.Linq.Lookup<>可公开访问,并且仅仅因为它没有公共构造函数并不意味着它不能用作返回类型。第二部分很好地解释了为什么返回类型是接口。
Waldfee

@Waldfee并非旨在公开提供。public从代码的角度来看,从技术上讲,它是可设计的,但并不是为了公开使用而设计的,因此您可以类视为内部类。
Servy

6

我认为返回aList<T>是由方法名称说明这一事实证明的ToList。否则,必须将其命名ToIList。此方法的目的是将非IEnumerable<T>特定类型转换为特定类型List<T>

如果您有一个名称不确定的方法,如GetResults,则返回类型如IList<T>IEnumerable<T>对我来说似乎合适。


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

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


2
但是为什么ToLookup()返回类型是一个接口呢?
Waldfee 2013年

3
@Waldfee:可能因为它返回的对象在逻辑上是不可变的,这意味着工厂可以根据它将包含的内容自由地优化返回类型。例如,如果要返回的实例只有一个项目,那么为其构建哈希表将很愚蠢。ToLookup在这种情况下,具有返回接口可以使其返回不同的具体类型。即使当前的实现无法优化这种情况,未来的实现也可以做到。
2013年

3

通常,当您在方法上调用ToList()时,您正在寻找具体的类型,否则该项目可能仍为IEnumerable类型。除非您要执行需要具体列表的操作,否则无需转换为列表。


5
@Default他没有发布到English.stackexchange.com ;-)
RB。

3
我不喜欢IList因为数组假装要实现它,但是当您调用IList诸如之类的特定方法时AddRemove您会得到一个异常:-(。当然,您可以测试该IsReadOnly属性,但在我看来,缩小范围(或至少缩小了范围)限制性的)的界面,而不是延伸它的是违反里氏替换原则的。
奥利弗Jacot-Descombes

消耗接口但返回实现是一个很好的API设计(无论如何通常都是通用规则;应禁止返回数组)。
莫滕·默特纳

@MortenMertner真的吗?我本以为那是另一回事。您有讨论此内容的参考资料吗?
RB。

3
@MortenMertner,如果您将函数的返回类型视为必须遵守的承诺,那么做出较小的承诺是有意义的。
Jodrell 2013年

3

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


2

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

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

}

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

如果您确实想返回IList,则没有什么可以阻止您使用自己的简单包装器:

public static IList<TSource> ToIList<TSource>(this IEnumerable<TSource> source)
{
    if (source == null) throw new ArgumentNullException(source);
    return source.ToList();
}

2

简短的答案是,一般而言,权威的《框架设计指南》建议返回最具体的可用类型。(对不起,我手头没有引用,但是我很清楚地记得这一点,因为它与Java社区指南相反,后者倾向于相反的引用)。

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

ToLookup<T>在人群中是独一无二的。但完全在指导原则之内:它图书馆作者愿意支持的最具体的类型(正如其他人指出的那样,具体Lookup<T>类型似乎更多是内部类型,不适合公众使用)。


2

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

返回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)在上使用List<T>,因为它存在于中,List<T>但不能在中使用IList<T>

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


1

如果一个函数返回一个新构造的不可变对象,则调用者通常不应该关心返回的精确类型,只要它能够保存其包含的实际数据即可。例如,本应返回an的函数IImmutableMatrix通常可以返回ImmutableArrayMatrix由私有数组支持的函数,但是如果所有单元格都为零,则它可能返回an ZeroMatrix,仅由WidthandHeight字段支持(使用getter则仅返回all时间)。调用者不会在乎它是ImmutableArrayMatrix矩阵还是ZeroMatrix; 这两种类型都将允许读取其所有单元格,并保证其值永远不会改变,而这正是调用方关心的。

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

顺便说一句,有点像是IEnumerator<T>.GetEnumerator()一种特殊情况。返回的对象几乎总是可变的,但是只能以非常严格的方式进行。实际上,期望返回的对象,无论其类型如何,都将恰好具有一种可变状态:它在枚举序列中的位置。虽然IEnumerator<T>预期an是可变的,但其状态在派生类实现中会有所不同的部分则不会。


相同的签名,相同的返回类型,不是吗?也许具体的实现会有所不同,但返回类型却不会。
Jodrell 2013年

@Jodrell:问题是,其当前实现总是返回特定类型的函数应该返回该精确类型,还是应该返回一个不太特定的类型。我认为工厂最好返回最适合将来版本可能要返回的所有内容的类型,并且在许多情况下,自由可变类型的工厂比返回广泛变化的类型的可能性要小得多。不可变或本质上不可变类型的工厂。
2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.