扩展方法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<>
扩展名的签名非常一致,除了这两种情况。我一直认为这有点奇怪。
此外,为了让事情更加混乱,documentation for ToLookup()
州:
根据a从IEnumerable创建查找 指定的键选择器功能。
但返回类型为ILookup<TKey, TElement>
。
在Edulinq中,Jon Skeet提到返回类型为List<T>
而非IList<T>
,但不会进一步触及主题。
广泛的搜索没有得到答案,所以在这里我问你:
是否有任何设计决策背后没有将返回值键入接口,或者它只是偶然发生?
答案 0 :(得分:49)
返回List<T>
的优势在于,List<T>
不属于IList<T>
的{{1}}方法很容易使用。 List<T>
使用IList<T>
可以做很多事情。{/ 1}}。
相比之下,Lookup<TKey, TElement>
只有一个ILookup<TKey, TElement>
没有的可用方法(ApplyResultSelector
),你可能最终也不会使用它。
答案 1 :(得分: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开发团队决定不返回接口。
不要觉得始终返回界面是最佳做法。如果您的代码或第三方代码需要这样的封装。
答案 2 :(得分:8)
List
超过IList
有很多好处。首先,List
包含IList
没有的方法。您还知道实现是什么,它允许您推断它的行为方式。你知道它可以有效地添加到最后,但不是开始,你知道它的索引器非常快,等等。
您无需担心您的结构被更改为LinkedList
并破坏应用程序的性能。当涉及到这样的数据结构时,在很多情况下确实知道如何实现数据结构非常重要,而不仅仅是它遵循的合同。这种行为不应该改变。
您也无法将IList
传递给接受List
的方法,这是您经常看到的内容。经常使用ToList
,因为此人确实需要List
的实例,以匹配他们无法控制的签名,而IList
对此无效。
然后我们问自己返回IList
有什么好处。好吧,我们可能会返回列表的其他一些实现,但如前所述,这可能会产生非常的不利后果,几乎可以肯定的是,使用任何其他类型都可能获得更多。它可能会给你温暖的模糊使用一个接口而不是一个实现,但即使这是我认为在这种情况下不是一个良好的心态(一般或)。通常,返回接口通常不优选返回具体实现。 “在你所接受的内容中保持自由,并在你提供的内容中具体说明。”在可能的情况下,方法的参数应该是定义您需要的最少功能的接口,您的调用者可以在任何实现您需要的实现中传递,并且您应该提供实现的具体内容,因为调用者是“允许”看到它们可以像对象那样对结果做同样多的事情。传递接口限制了该值,这只是偶尔你想要做的事情。
现在我们继续讨论,“为什么要返回ILookup
而不是Lookup
?”好吧,首先关闭Lookup
不是公共课。 Lookup
中没有System.Collections.*
。通过LINQ公开的Lookup
类不公开公开构造函数。除了通过ToLookup
之外,你不能能使用该课程。它还没有公开任何尚未通过ILookup
公开的功能。在这个特殊情况下,他们专门围绕这个确切的方法(ToLookup
)设计了接口,而Lookup
类是专门为实现该接口而设计的类。因为所有这些,所讨论的关于List
的所有要点都不适用于此。返回Lookup
会不会是问题,不是,不是真的。在这种情况下,它无论如何都无关紧要。
答案 3 :(得分:6)
在我看来,通过方法名称List<T>
返回ToList
是合理的。否则,它必须被命名为ToIList
。此方法的目的是将非特定IEnumerable<T>
转换为特定类型List<T>
。
如果你的方法有一个非特定的名称,例如GetResults
,那么像IList<T>
或IEnumerable<T>
这样的返回类型对我来说似乎是合适的。
如果你看一下带有反射器的Lookup<TKey, TElement>
类的实现,你会看到很多internal
成员,只有LINQ本身才能访问它们。没有公共构造函数,Lookup
对象是不可变的。因此,直接公开Lookup
没有任何优势。
Lookup<TKey, TElement>
类似乎是LINQ内部类型,不适合公共使用。
答案 4 :(得分:3)
通常,当您在方法上调用ToList()时,您正在寻找具体类型,否则该项可能保持为IEnumerable类型。除非您正在执行需要具体列表的操作,否则无需转换为List。
答案 5 :(得分:3)
我相信返回List&lt;&gt;的决定而不是IList&lt;&gt;是调用ToList的一个更常见的用例是强制立即评估整个列表。通过返回列表&lt;&gt;这是有保证的。使用 IList &lt;&gt;实现仍然可能是懒惰的,这会破坏呼叫的“主要”目的。
答案 6 :(得分: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();
}
答案 7 :(得分:2)
简短的回答是,一般来说,权威Framework Design Guidelines建议返回最具体的类型。 (对不起,我手边没有引文,但我清楚地记得这一点,因为它与Java社区指南形成鲜明对比,后者更倾向于相反)。
这对我有意义。你可以随时做IList<int> list = x.ToList()
,只有图书馆作者需要关心能否支持具体的返回类型。
ToLookup<T>
是人群中独一无二的。但完全符合指南: 是图书馆作者愿意支持的最具体的类型(正如其他人所指出的那样,具体的Lookup<T>
类型似乎更像是内部的类型不适合公共使用)。
答案 8 :(得分: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
作为List<T>
返回使我们能够在List<T>
本身之外调用所有这些接口上的成员。例如,我们只能在List.BinarySearch(T)
上使用List<T>
,因为它存在于List<T>
中,但不存在于IList<T>
中。
一般来说为了最大限度地提高方法的灵活性,我们应该把最抽象的类型作为参数(即只有我们将要使用的东西)并返回最不抽象的类型可能(允许更多功能的返回对象)。
答案 9 :(得分:1)
如果函数返回一个新构造的不可变对象,调用者通常不应该关心返回的精确类型,前提是它能够保存它包含的实际数据。例如,应该返回IImmutableMatrix
的函数通常会返回由私有数组支持的ImmutableArrayMatrix
,但如果所有单元格都保留为零,则可能会返回ZeroMatrix
,仅由Width
和Height
字段支持(使用一直只返回零的getter)。调用者不关心它是否被赋予ImmutableArrayMatrix
矩阵或ZeroMatrix
;这两种类型都会允许读取所有单元格,并保证它们的值永远不会改变,这就是调用者所关心的。
另一方面,返回允许开放式变异的新构造对象的函数通常应该返回调用者期望的精确类型。除非有调用者可以请求不同返回类型的方法(例如,通过调用ToyotaFactory.Build("Corolla")
与ToyotaFactory.Build("Prius")
),否则没有理由将声明的返回类型作为其他任何内容。虽然返回不可变数据保持对象的工厂可以根据要包含的数据选择类型,但返回自由可变类型的工厂将无法知道可以将哪些数据放入其中。如果不同的呼叫者有不同的需求(例如,回到现有的例子,一些呼叫者的需求会被数组所满足,而其他人则不会),他们应该选择工厂方法。
IEnumerator<T>.GetEnumerator()
之类的东西有点特殊。返回的对象几乎总是可变的,但只是以非常高度约束的方式;实际上,预期返回的对象无论其类型如何都将具有一个可变状态:它在枚举序列中的位置。虽然预期IEnumerator<T>
是可变的,但它的状态部分在派生类实现中会有所不同。