我为什么要用IEnumerable来处理…比如说List呢?前者比后者有什么优势?
- 这一定是副本。
- 请参见:stackoverflow.com/questions/3628425/…
IEnumerable是一个接口,它告诉我们可以枚举一系列T实例。如果需要允许某人查看并为集合中的每个对象执行一些操作,这就足够了。
另一方面,List是IEnumerable的特定实现,它以特定的已知方式存储对象。在内部,这可能是存储通过IEnumerable公开的值的非常好的方法,但List并不总是合适的。例如,如果您不需要按索引访问项,而是在集合的开始处不断插入项,然后从结尾处删除项,那么使用Queue将更为合适。
通过在API中使用IEnumerable,您可以随时灵活地更改内部实现,而不必更改任何其他代码。这在允许您的代码具有灵活性和可维护性方面有巨大的好处。
- 我强烈同意您关于与实现脱钩的观点,但我也认为有一点需要明确预期的用法;即使我有一个列表,将其称为IEnumerable,也清楚地表明了我在该用法中从集合期望的功能是可枚举功能,而不是任何其他功能。ER列表功能。它实际上被用作意图的文档;我发现那些明确的意图文档倾向于提供您指出的完全分离。
- @保罗:我同意,不过我建议用IList对IEnumerable,在这种情况下,不要用List。
- 好的一点,尽管这提出了"接口与具体实例"的另一个问题,但我认为这是一个不同的问题。
在这一点上,杰弗里·里克特写道:
在声明方法的参数类型时,应指定尽可能最弱的类型,首选接口而不是基类。例如,如果编写的方法操作项集合,最好通过使用IEnumerable等接口,而不是使用List等强数据类型,甚至使用ICollection或IList等强接口类型:
1 2 3 4 5
| // Desired: This method uses a weak parameter type
public void ManipulateItems<T>(IEnumerable<T> collection) { ... }
// Undesired: This method uses a strong parameter type
public void ManipulateItems<T>(List<T> collection) { ... } |
当然,原因是有人可以调用传入数组对象、List对象、String对象等的第一个方法—任何类型实现IEnumerable的对象。第二种方法只允许传入List对象;它不接受数组或String对象。显然,第一种方法更好,因为它更灵活,可以在更广泛的场景中使用。
当然,如果编写的方法需要一个列表(而不仅仅是任何可枚举对象),那么应该将参数类型声明为IList。您仍然应该避免将参数类型声明为List。使用IList允许调用者传递数组和任何其他类型实现IList的对象。
另一方面,通常最好使用尽可能强的类型来声明方法的返回类型(尽量不要将自己提交到特定的类型)。
- 是的,但是要小心使用IList,因为数组只部分实现它,例如,调用Add或Remove会抛出数组的异常。
- @Olivierjacot Descombes:数组有一个更微妙的问题:一个Cat[]可以作为IList myList传递,如果Animal someAnimal为空或恰好持有对Cat的引用,myList[0]=someAnimal将成功。但是,如果someAnimal是对Cat以外的某个对象的非空引用,则操作将失败。
- @Olivierjacot Descombes:除了使用反射,没有办法知道允许通过索引存储给定类型的任何内容的IList是否允许通过索引存储该类型的所有内容。
- @Supercat:是的,让数组和反变形数组在我看来是一个严重的设计缺陷。另外,数组不应该实现IList。相反,应该有一个IArray接口,IList将扩展该接口。
- @Olivierjacot descombes:数组只是协变的;为数组定义一个接口是很困难的,它允许一种有效的类型不可知的排列方式;相比之下,简单地允许数组协变更容易。理想的情况是为阵列提供多种存储位置类型(例如给定的S:T:U和T[] foo; T+[] bar, T-[] boz;,foo只能保存T[]的实例,bar只能保存T[]或S[]的实例,boz可以保存T[]或U[]的实例),但是.NET期望有很强的相关性。堆对象类型中存储位置类型之间的操作。
- @supercat:IArray将实现索引器和Length属性。呼叫地点的差异可以解决这个问题:void MyMethod(Animal[] out a) { ... }和void MyMethod(Cat[] in c) { ... }。但是根据埃里克·利珀特的说法,这会给C编译器增加很大的复杂性。
- @Olivierjacot Descombes:如果一个Sort例程同时读取和写入一个集合,那么它需要什么样的变化呢?如果一个方法希望ISortableList以完全类型安全的方式接受和排序Cat[],该怎么办?一种方法是让ISortableList继承一个非泛型IPermutableList,其中包括一个类型不可知的SwapAt方法,但如果某些排序或其他排列算法能够读取某些内容并随后将其写回,则可以最有效地实现。
- 一个可以有一个IPermutableList包括一个GetBorrower方法,它将返回一个类型不可知的IArrayItemBuffer,反过来,它将有ReadItemAt、SwapWithItemAt和WriteItemAt方法,可以在数组和内部缓冲区之间移动项目;这可能是有点效率的,但仍然有点笨拙。与之合作。
- @supercat:排序时,我们只使用集合中已经存在的对象,因此我们知道可以将它们安全地强制转换到T。此外,如果我们使用此集合的排序方法,则在排序期间,项目不会在任何时间离开集合,因此差异不是问题。无论如何,这些项目将被视为IComparable或IComparable。
- @OlivierJacot Descombes:如果sort方法从Animal[]中读取Animal,它知道将该引用存储回数组是安全的,但是编译器没有"从特定Animal数组中读取的东西"的概念。对于表示"某些Animal派生的可排序列表"的接口类型,它必须提供一个"store Animal方法,如果给定的内容无效,该方法可能在运行时失败,或者提供一种类型安全的方法,从数组中读取并写回内容。
- @Supercat:我的观点是,您只需要在没有泛型的语言中使用协变arryas。在爪哇上看到协变数组,在维基百科上看到C。
- @OlivierJacot Descombes:如果有一个可用的泛型类型参数与数组的确切类型相匹配,那么指定的方法将有效。但是,它不允许将数组添加到异类集合中,然后对它们进行排序(如果将Dog[]和SiameseCat[]添加到ISomething的集合中,则应该可以对Dog[]中的所有元素和SiameseCat[]中的所有元素进行排序,但唯一可用的通用类型参数是Animal。()数组T[]可以支持类型安全的IPermutableArray,足以进行排序…
- …不必知道数组的确切类型(因此,包含指向SiameseCat[]的IPermutableArray类型引用的代码,以及IComparer类型的代码可以对数组进行排序,而不必知道它是SiameseCat[]类型,但IPermutableArray的灵活有效设计需要一些考虑。
- @supercat:很明显,从数组中删除方差是一个限制,但问题是收益是否大于限制。参见埃里克·利珀特的博客。
- @我读过利珀特先生的博客。我的观点不是数组引用应该是协变的,而是数组除了协变的只读和不变的可变接口之外,还应该实现或多个协变可变接口。有时,知道T[]可以容纳T类型的所有东西是很有帮助的,这两种类型有时都有助于在编译时允许在运行时成功或不成功的事情。作为另一个例子,…
- …基类型可以保证虚拟方法的任何派生类实现(如WithSomeProperty(xx)将返回同一派生类型的实例。如果我有一个从包含WithSomeProperty()方法的类继承的集合,那么能够说一些像for (int i=0; i这样的话会很有帮助,因为认识到如果WithSomeProperty违反了它的合同,在运行时写入可能会失败。不幸的是,我知道表达这样一个概念的唯一干净的方法…
- …将有IReadWriteByIndex,IWriteByIndex,和IReadWriteByIndex,其中胎面:twrite。从方差的角度来看,这会很好地工作,但是对于索引GET来说,没有办法返回一个TRead,同时允许setter接受一个TWrite。
- @超级卫星:好的,明白了!
- +这是一个很好的答案,谢谢。顺便说一下,里克特的参考资料在第233页。
使用迭代器的概念,您可以在速度和内存使用方面,实现算法质量的重大改进。
让我们考虑下面两个代码示例。两者都分析文件,一个在集合中存储行,另一个使用可枚举的。
第一个例子是O(n)时间和O(n)内存:
1 2 3
| IEnumerable<string> lines = SelectLines();
List<Item> items = lines.Select(l=>ParseToItem(l)).ToList();
var itemOfIterest = items.FirstOrDefault(IsItemOfIterest); |
第二个例子是O(n)时间,O(1)内存。此外,即使渐进时间复杂性仍然是O(n),它加载的项平均比第一个示例少两倍:
1
| var itemOfIterest = lines.FirstOrDefault(l=>IsItemOfIterest(ParseToItem(l)); |
这是selectlines()的代码
1 2 3 4 5 6 7
| IEnumerable<string> SelectLines()
{
...
using(var reader = ...)
while((line=reader.ReadLine())!=null)
yield return line;
} |
这就是为什么它加载的项平均比第一个示例少两倍。假设在文件范围内的任何位置查找元素的概率都相同。如果是IEnumerable,则只从文件中读取到感兴趣元素的行。在tolist调用可枚举的情况下,甚至在开始搜索之前都会读取整个文件。
当然,第一个示例中的列表将保存内存中的所有项,这就是O(N)内存使用的原因。
集合的不同实现可以是可枚举的;使用IEnumerable可以清楚地表明,您感兴趣的是可枚举性,而不是集合的基础实现的结构。
正如Copsey先生所提到的,这有提供与实现的脱钩的好处,但我的论点是,接口功能的最小子集的清晰定义(即,尽可能使用IEnumerable而不是列表)提供了确切的脱钩,同时也需要适当的设计理念。也就是说,您可以实现去耦,但不能实现最小的依赖,但如果没有实现最大的去耦,就不能实现最小的依赖。
如果您计划构建一个公共API,那么最好使用IEnumerable而不是List,因为您最好使用最简单的接口/类。如果需要,可以使用List按索引访问对象。
这里有一个很好的指导方针,在什么时候使用IEnumerable,ICollection,List等等。
通常不直接使用IEunumerable。它是您更可能使用的许多其他集合的基类。例如,IEnumerable提供了使用foreach通过集合循环的能力。它被许多继承类使用,如List。但是,IEnumerable不提供排序方法(尽管您可以使用linq),而其他一些泛型集合(如List也有这种方法。
哦,当然可以,您可以使用它创建自己的自定义集合类型。但对于日常用品来说,它可能不如从中获得的收藏品有用。
为什么在类中实现IEnumerable?
如果您正在编写一个类,并且您的类实现了IEnumerable接口(generic(t)或not),那么您允许类的任何使用者在不知道其结构的情况下迭代其集合。
LinkedList的实现方式与队列、堆栈、二叉树、哈希表、图形等不同。类表示的集合的结构可能不同。
作为一个"使用者"(如果您正在编写一个类,并且您的类使用/利用了一个实现IEnumerable的类对象),您可以使用它,而不必关心它是如何实现的。有时,Consumer类并不关心实现——它只是想检查所有的项(打印它们?改变它们?比较它们?等)
(所以作为一个消费者,如果你的任务是遍历binarytree类中的所有项,而你跳过了data-structures-101中的课程-如果binarytree编码器实现了IEnumerable-你就走运了!您不必打开一本书并学习如何遍历树,只需在该对象上使用foreach语句即可完成。)
作为一个"生产者"(编写一个包含数据结构/集合的类),您可能不希望类的消费者处理它的结构(担心他们可能会破坏它)。因此,您可以将集合设置为私有的,并且只公开一个公共的IEnumerator。
它还允许某种一致性——集合可能有几种方法迭代其项(预排序、无序、后排序、广度优先、深度优先等)——但IEnumerable只有1个实现。您可以使用它来设置在集合上迭代的"默认"方式。
为什么在方法中使用IEnumerable?
如果我编写了一个获取集合的方法,对它进行迭代,并对这些项(聚合它们吗?比较它们?等等)为什么我应该把自己限制在1种类型的收藏中?
写这个方法public void Sum(List list) {...}来合计集合中的所有项目意味着我只能接收一个列表并对其进行合计。写这个EDOCX1[1]意味着我可以取任何实现IEnumerable的对象(列表、队列、堆栈等),并对它们的所有项进行合计。
其他注意事项
还有延迟执行和非托管资源的问题。IEnumerable使用yield语法,这意味着您可以自己检查每个项,并可以在前后执行各种计算。同样,这种情况会一个接一个发生,所以你不必在开始时保存所有的收集。在枚举开始之前(即在运行foreach循环之前),不会实际执行计算。在某些情况下,这可能更有用,更有效。例如,您的类可能不会在内存中保存任何集合,而是遍历特定目录中的所有文件、特定数据库中的项或其他非托管资源。IEnumerable可以介入并为您做到这一点(您也可以在没有IEnumerable的情况下做到这一点,但IEnumerable在概念上"符合",而且它还为您提供了能够使用foreach循环中生成的对象的好处)。
IEnumerable提供了在对象集合上实现自己的存储和迭代逻辑的方法。
IEnumerable利用了这里解释的延迟执行:IEnumerable vs list-要使用什么?它们是如何工作的?
IEnumerable为称为协方差的数组类型启用隐式引用转换。请考虑以下示例:
公共抽象类车辆{}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Car :Vehicle
{
}
private void doSomething1 (IEnumerable <Vehicle > vehicles )
{
}
private void doSomething2 (List <Vehicle > vehicles )
{
}
var vec = new List <Car >();
doSomething1 (vec ); // this is ok
doSomething2 (vec ); // this will give a compilation error |
IEnumerable的实现通常是类指示它应可用于"foreach"循环的首选方法,并且同一对象上的多个"foreach"循环应独立操作。虽然IEnumerable除了"foreach"之外还有其他用法,但通常表示应该实现IEnumerable的方法是,在类中说"foreach foo in classitem foo.do ou something();"。