为什么IEnumerable <T> .ToList <T>()返回List <T>而不是IList <T>?
扩展方法ToList()
返回一个List<TSource>
。 ToDictionary()
返回一个Dictionary<TKey, TSource>
。
我很好奇为什么这些方法不分别inputIList<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); }
那么为什么这种方法将其返回值作为接口的特定实现而不是接口本身呢? 唯一的变化是返回types。
我很好奇,因为IEnumerable<>
扩展名在它们的签名中非常一致,除了这两种情况。 我一直认为这有点奇怪。
另外,为了使事情更令人困惑, ToLookup()
的文档声明:
根据指定的键select器函数从IEnumerable创build一个Lookup。
但返回types是ILookup<TKey, TElement>
。
在Edulinq中 ,Jon Skeet提到返回types是List<T>
而不是IList<T>
,但是并没有进一步触及这个主题。
广泛的search没有得到答案,所以在这里我问你:
是否有任何devise决定背后没有键入作为接口的返回值,还是只是偶然?
返回List<T>
的好处是,那些不是IList<T>
一部分的List<T>
方法很容易使用。 你可以用List<T>
做很多事情,你不能用IList<T>
。
相比之下, Lookup<TKey, TElement>
只有一个可用的方法, ILookup<TKey, TElement>
没有( ApplyResultSelector
),并且您可能不会最终使用该方法。
这种决定可能会感到任意,但我想ToList()
返回List<T>
而不是一个接口,因为List<T>
都实现IList<T>
但它添加了其他成员不存在于一个普通的IList<T>
-table目的。
例如, 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 dev团队决定不返回一个界面。
不觉得总是返回一个界面是最好的做法。 这是如果你的代码或第三方需要这样的封装。
有一个List
比IList
有很多好处。 首先, List
有IList
没有的方法。 你也知道这个实现是什么使你能够推断它的行为。 你知道它可以有效地添加到最后,但不是开始,你知道它的索引是非常快的,等等。
您不必担心您的结构被更改为LinkedList
并破坏应用程序的性能。 当谈到像这样的数据结构时,在很多情况下,知道你的数据结构是如何实现的,而不仅仅是它遵循的契约是非常重要的。 这是不应该改变的行为。
你也不能把一个IList
传递给接受List
的方法,这是你看到的很多东西。 ToList
经常被使用,因为这个人确实需要一个List
实例来匹配他们无法控制的签名,而IList
对此没有帮助。
然后我们问自己,返回IList
有什么好处。 那么,我们可能会返回一个其他的实现列表,但如前所述,这可能会有非常有害的后果,几乎肯定比使用任何其他types可能获得的更多。 它可能会给你一个使用接口而不是一个实现的温暖模糊,但即使这样,我不觉得这是一个好的心态(一般来说)或者在这种情况下。 通常返回一个接口通常不会比返回一个具体的实现更可取。 “在你所接受的东西中保持自由,并且在你所提供的东西中具体。” 在可能的情况下,方法的参数应该是定义最less量的function的接口,调用者可以在任何需要它的实现中传递这些函数,而且应该提供具体的调用方法“允许”看到,以便他们可以尽可能多地处理该对象所能达到的结果。 传递一个接口会限制这个值,这只是偶尔你想要做的事情。
所以,现在我们进入“为什么要返回ILookup
而不是Lookup
?” 那么,首先Lookup
不是公开课。 System.Collections.*
没有Lookup
System.Collections.*
。 通过LINQ公开的Lookup
类公开地公开没有构造函数。 除非通过ToLookup
否则无法使用该类。 它也暴露了没有通过ILookup
暴露的ILookup
。 在这个特定的情况下,他们专门devise了这个接口( ToLookup
),而Lookup
类是专门为实现这个接口而devise的类。 由于这几乎所有关于List
讨论的点都不适用于此。 返回Lookup
会是一个问题吗?不,不是。 在这种情况下,任何一种方式都不重要。
在我看来,返回一个List<T>
是有道理的,方法名称是ToList
。 否则,它将被命名为ToIList
。 这种方法的目的是将非特定的IEnumerable<T>
转换为特定typesList<T>
。
如果你有一个像GetResults
这样的非特定名字的方法,那么像IList<T>
或IEnumerable<T>
这样的返回types对我来说似乎是合适的。
如果您查看带reflection器的Lookup<TKey, TElement>
类的实现,您将看到许多internal
成员,只有LINQ本身才能访问它。 没有公共构造函数, Lookup
对象是不可变的。 因此直接暴露Lookup
没有任何优势。
Lookup<TKey, TElement>
类似乎是LINQ内部的一种,不适合公共使用。
一般情况下,当你在一个方法上调用ToList()时,你正在寻找一个具体的types,否则该项可以保持为IEnumerabletypes。 你不需要转换为列表,除非你正在做一些需要具体列表的东西。
我相信决定返回一个List <>而不是一个IList <>是调用ToList的更常见的用例之一是强制立即评估整个列表。 通过返回一个List <>,这是有保证的。 对于IList <>,实现仍然可能是懒惰的,这将打败调用的“主要”目的。
由于List<T>
实际上实现了一系列接口,而不仅仅是IList:
public class List<T> : IList<T>, ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable{ }
每个接口都定义了List必须符合的一系列function。 select一个特定的,将导致大部分的实现无法使用。
如果你确实想要返回IList,没有什么能阻止你拥有你自己的简单包装:
public static IList<TSource> ToIList<TSource>(this IEnumerable<TSource> source) { if (source == null) throw new ArgumentNullException(source); return source.ToList(); }
简单的回答是,总体来说,权威的框架devise指南推荐了最具体的types。 (对不起,我手边没有引用,但是我清楚地记得这一点,因为它与Java社区的指导原则相反)。
这对我有意义。 你总是可以做的,例如IList<int> list = x.ToList()
,只有库作者需要关心能否支持具体的返回types。
ToLookup<T>
是人群中唯一的一个。 但是完全符合指南:它是图书馆作者愿意支持的最具体的types(正如其他人指出的那样,具体的Lookup<T>
types看起来更像是一种内部types,并不意味着公共用途)。
这是程序员难以理解接口和具体types使用的常见事情之一。
返回一个实现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>
。
一般来说,为了最大限度地提高我们方法的灵活性, 我们应该把大多数抽象types作为参数 (即只有我们将要使用的东西) 并返回最less的抽象types (以允许更多的function返回对象)。
如果函数返回一个新构build的不可变对象,调用者通常不应该关心返回的确切types,只要它能够保存它所包含的实际数据即可。 例如,一个应该返回一个IImmutableMatrix
的函数通常会返回一个由一个私有数组支持的ImmutableArrayMatrix
,但是如果所有的单元格都保持为零,它可能会返回一个ZeroMatrix
,仅由Width
和Height
字段支持(使用getter它总是返回零)。 调用者不会在意是否给了一个ImmutableArrayMatrix
matrix或一个ZeroMatrix
; 这两种types都可以让所有的单元格被读取,并保证它们的值永远不会改变,这就是调用者所关心的。
另一方面,返回允许开放式变异的新构build的对象的函数通常应返callback用者期望的精确types。 除非调用者可以请求不同的返回types(例如,通过调用ToyotaFactory.Build("Corolla")
和ToyotaFactory.Build("Prius")
),否则没有任何理由使声明的返回types成为别的东西。 虽然返回不可变数据保持对象的工厂可以根据要包含的数据select一个types,但返回可自由变化types的工厂将无法知道可以将哪些数据放入其中。 如果不同的呼叫者有不同的需求(例如回到现有的例子,一些呼叫者的需求可能会遇到一个arrays,而另一些则不会),他们应该select工厂方法。
顺便说一句,像IEnumerator<T>.GetEnumerator()
有点特殊的情况。 返回的对象几乎总是可变的,但只是以非常严格的方式; 的确,不pipe它的types如何,返回的对象都会有一个可变的状态:它在枚举序列中的位置。 虽然IEnumerator<T>
预计是可变的,但是派生类实现中状态的不同部分却不是。