为什么我应该使用 IEnumerable<T>
,而不是只用 List<T>
?前者相对于后者的优势是什么?
为什么我应该使用 IEnumerable<T>
,而不是只用 List<T>
?前者相对于后者的优势是什么?
IEnumerable<T>
是一个接口,它告诉我们可以枚举一系列的T
实例。如果你需要允许某人看到并对集合中的每个对象执行某些操作,那么这就足够了。
另一方面,List<T>
是IEnumerable<T>
的一个具体实现,它以特定的、已知的方式存储对象。在内部,这可能是一种非常好的方式来存储您通过IEnumerable<T>
公开的值,但并不总是适用于List<T>
。例如,如果您不需要按索引访问项,而是不断在集合开头插入项,然后从末尾删除项,那么使用Queue<T>
会更加合适。
通过在API中使用IEnumerable<T>
,您为自己提供了随时更改内部实现的灵活性,而无需更改任何其他代码。这在允许您的代码具有灵活性和可维护性方面具有巨大的优势。
IList<T>
而不是IEnumerable<T>
,不要使用List<T>
。 - Reed Copsey关于这一点,Jeffrey-Richter 写道:
在声明方法的参数类型时,应该尽可能地指定最弱的类型,优先使用接口而不是基类。例如,如果你正在编写一个操作项目集合的方法,最好使用接口 IEnumerable<T>
来声明方法的参数,而不是使用强类型如 List<T>
或者更强的接口类型如 ICollection<T>
或 IList<T>
:
// 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) { ... }
Cat[]
可能会被传递为 IList<Animal> myList
,如果 Animal someAnimal
是 null 或者恰好持有对 Cat
的引用,那么 myList[0]=someAnimal
将成功。但是,如果 someAnimal
是对除 Cat
以外的其他东西的非空引用,则该操作将失败。 - supercatIList<T>
是否允许存储该类型的所有内容。 - supercatIList<T>
。相反,应该有一个IArray<T>
接口,它将扩展IList<T>
。 - Olivier Jacot-DescombesIArray<T>
将实现索引器和 Length
属性。调用方差异将解决问题:void MyMethod(Animal[] out a) { ... }
和 void MyMethod(Cat[] in c) { ... }
。但根据 Eric Lippert 的说法,这将给 C# 编译器增加很大的复杂性。 - Olivier Jacot-DescombesSiameseCat[]
的IPermutableArray<Animal>
类型引用的代码,以及一个IComparer<Animal>
,可以对数组进行排序,而无需知道它是一个SiameseCat[]
),但是对于IPermutableArray<out T>
的灵活高效设计需要一些思考。 - supercat使用迭代器的概念,您可以在算法质量方面实现重大改进,包括速度和内存使用。
让我们考虑以下两个代码示例。两者都解析文件,一个将行存储在集合中,另一个使用可枚举对象。
第一个示例的时间复杂度为O(N),空间复杂度为O(N):
IEnumerable<string> lines = SelectLines();
List<Item> items = lines.Select(l=>ParseToItem(l)).ToList();
var itemOfIterest = items.FirstOrDefault(IsItemOfIterest);
第二个例子的时间复杂度为O(N),内存复杂度为O(1)。此外,即使渐近时间复杂度仍然为O(N),平均而言,它加载的项数只有第一个例子的一半:
var itemOfIterest = lines.FirstOrDefault(l=>IsItemOfIterest(ParseToItem(l));
以下是SelectLines()函数的代码:
IEnumerable<string> SelectLines()
{
...
using(var reader = ...)
while((line=reader.ReadLine())!=null)
yield return line;
}
以下是为什么它平均加载的项数只有第一个示例的一半。假设在文件范围内找到元素的概率相同。对于IEnumerable,在读取感兴趣的元素之前,只会读取该文件中的前几行。而在枚举上调用ToList时,甚至在开始搜索之前就会读取整个文件。
当然,第一个示例中的List将保存所有的项以供随时在内存中使用,这就是O(N)内存使用量的原因。
IEnumerable
接口。它是许多其它集合类的基类,而这些集合类更适合你使用。例如,IEnumerable
接口提供了使用 foreach
循环遍历集合的能力。这个特性被许多从它继承而来的类所使用,比如 List<T>
。但是,IEnumerable
接口并没有提供排序方法(虽然你可以使用 Linq 来实现),而一些其它泛型集合类,如 List<T>
,却拥有这个方法。IEnumerable提供了一种实现自己的存储和迭代对象集合逻辑的方式。
为什么要在类中实现IEnumerable接口?
如果你正在编写一个类,并且你的类实现了IEnumerable接口(泛型(T)或非泛型),那么你允许任何使用你的类的消费者在不知道其结构的情况下遍历它的集合。
链表的实现方式与队列、栈、二叉树、哈希表、图等不同。由你的类表示的集合可能以不同的方式进行结构化。
作为“消费者”(如果你正在编写一个类,并且你的类使用/利用实现IEnumerable的类对象),你可以使用它而不必关心它是如何实现的。有时,消费者类并不关心实现方式——它只想遍历所有项(打印它们?更改它们?比较它们?等等)
(因此,作为消费者,如果你的任务是遍历BinaryTree类中的所有项,并且你在数据结构101课程中跳过了该课程——如果BinaryTree编码器实现了IEnumerable——你很幸运!你不必打开一本书学习如何遍历树——只需在该对象上使用foreach语句即可完成。)
作为“生产者”(编写一个包含数据结构/集合的类),您可能不希望类的使用者处理它的结构(可能担心他们会破坏它)。因此,您可以将集合设置为私有,并仅公开一个公共IEnumerator。实现IEnumerable<T>通常是类表明它应该可用于“foreach”循环的首选方式,并且同一对象上的多个“foreach”循环应该独立运行。虽然除了“foreach”之外还有其他使用IEnumerable<T>的用途,但正常的指示是应该实现IEnumerable的类是其中一个,在这种类中,说“foreach foo in classItem {foo.do_something();}”是有意义的。
Enumerable利用延迟执行的优势,如此处所述:IEnumerable vs List - What to Use? How do they work?
IEnumerable使数组类型能够进行隐式引用转换,这被称为协变。请考虑以下示例:
public abstract class Vehicle { }
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