IEnumerable<T> 是什么,为什么我要使用它?它在编程中的作用是什么?

14

为什么我应该使用 IEnumerable<T>,而不是只用 List<T>?前者相对于后者的优势是什么?


1
请参考:https://dev59.com/5XA65IYBdhLWcg3wyh57 - Rezoan
10个回答

33

IEnumerable<T>是一个接口,它告诉我们可以枚举一系列的T实例。如果你需要允许某人看到并对集合中的每个对象执行某些操作,那么这就足够了。

另一方面,List<T>IEnumerable<T>的一个具体实现,它以特定的、已知的方式存储对象。在内部,这可能是一种非常好的方式来存储您通过IEnumerable<T>公开的值,但并不总是适用于List<T>。例如,如果您不需要按索引访问项,而是不断在集合开头插入项,然后从末尾删除项,那么使用Queue<T>会更加合适。

通过在API中使用IEnumerable<T>,您为自己提供了随时更改内部实现的灵活性,而无需更改任何其他代码。这在允许您的代码具有灵活性和可维护性方面具有巨大的优势。


2
我非常赞同你关于解耦实现的观点,但我也认为明确预期使用方式很重要;即使我有一个List<T>,将其称为IEnumerable<T>可以清楚地表明我在该使用中期望从集合中获得的功能是可枚举性功能,而不是任何其他List功能。这实际上是使用意图的文档化;我发现这些明确的意图说明通常提供了你所指出的那种解耦。 - Paul Sonier
2
@Paul:我同意,但在这种情况下,我建议使用IList<T>而不是IEnumerable<T>不要使用List<T> - Reed Copsey
1
好观点,虽然这带出了一个完全不同的问题:“接口 vs. 具体实例”,我认为这是一个不同的问题。 - Paul Sonier

4

关于这一点,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) { ... }

当然,原因是第一个方法可以传递数组对象、List对象、String对象等任何类型实现IEnumerable的对象。而第二个方法只允许传递List对象,不接受数组或String对象。显然,第一个方法更好,因为它更加灵活,可以在更广泛的场景中使用。
当然,如果你编写的方法需要列表(而不仅仅是任何可枚举对象),那么应将参数类型声明为IList。仍然应避免将参数类型声明为List。使用IList允许调用者传递数组和任何其他类型实现IList的对象。
另一方面,通常最好通过使用尽可能强的类型来声明方法的返回类型(尽量不要承诺特定类型)。

@OlivierJacot-Descombes:数组有一个更微妙的问题:Cat[] 可能会被传递为 IList<Animal> myList,如果 Animal someAnimal 是 null 或者恰好持有对 Cat 的引用,那么 myList[0]=someAnimal 将成功。但是,如果 someAnimal 是对除 Cat 以外的其他东西的非空引用,则该操作将失败。 - supercat
除了使用反射,没有其他方法可以知道一个允许通过索引存储给定类型的任何内容的 IList<T> 是否允许存储该类型的所有内容。 - supercat
@supercat:是的,在我看来,使数组协变和逆变是一个严重的设计缺陷。另外,数组不应该实现IList<T>。相反,应该有一个IArray<T>接口,它将扩展IList<T> - Olivier Jacot-Descombes
@supercat: IArray<T> 将实现索引器和 Length 属性。调用方差异将解决问题:void MyMethod(Animal[] out a) { ... }void MyMethod(Cat[] in c) { ... }。但根据 Eric Lippert 的说法,这将给 C# 编译器增加很大的复杂性。 - Olivier Jacot-Descombes
不需要知道数组的确切类型(因此,持有指向SiameseCat[]IPermutableArray<Animal>类型引用的代码,以及一个IComparer<Animal>,可以对数组进行排序,而无需知道它是一个SiameseCat[]),但是对于IPermutableArray<out T>的灵活高效设计需要一些思考。 - supercat
显示剩余14条评论

2
不同的集合实现可以枚举; 使用IEnumerable可以清楚地表明您感兴趣的是可枚举性,而不是集合底层实现的结构。
如Copsey先生所述,这有利于与实现解耦,但我认为尽可能清晰地定义接口功能的最小子集(即在可能的情况下使用IEnumerable而不是List)提供了确切的解耦,同时还需要适当的设计哲学。也就是说,您可以实现解耦但不能实现最小依赖性,但是您无法实现最小依赖性而不实现最大解耦。

2
如果您计划构建公共API,最好使用`IEnumerable`而不是`List`,因为您应该使用最简化的接口/类。 `List`允许您按索引访问对象(如果需要)。这里提供了一个非常好的指南Here,告诉你何时使用`IEnumerable`、`ICollection`、`List`等。

2

使用迭代器的概念,您可以在算法质量方面实现重大改进,包括速度和内存使用。

让我们考虑以下两个代码示例。两者都解析文件,一个将行存储在集合中,另一个使用可枚举对象。

第一个示例的时间复杂度为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)内存使用量的原因。


1
通常情况下,你不会直接使用 IEnumerable 接口。它是许多其它集合类的基类,而这些集合类更适合你使用。例如,IEnumerable 接口提供了使用 foreach 循环遍历集合的能力。这个特性被许多从它继承而来的类所使用,比如 List<T>。但是,IEnumerable 接口并没有提供排序方法(虽然你可以使用 Linq 来实现),而一些其它泛型集合类,如 List<T>,却拥有这个方法。
当然,你可以用它来创建自定义的集合类型。但对于日常使用而言,它可能没有从它派生的那些集合类那么有用。

0

IEnumerable提供了一种实现自己的存储和迭代对象集合逻辑的方式。


0

为什么要在类中实现IEnumerable接口?

如果你正在编写一个类,并且你的类实现了IEnumerable接口(泛型(T)或非泛型),那么你允许任何使用你的类的消费者在不知道其结构的情况下遍历它的集合。

链表的实现方式与队列、栈、二叉树、哈希表、图等不同。由你的类表示的集合可能以不同的方式进行结构化。

作为“消费者”(如果你正在编写一个类,并且你的类使用/利用实现IEnumerable的类对象),你可以使用它而不必关心它是如何实现的。有时,消费者类并不关心实现方式——它只想遍历所有项(打印它们?更改它们?比较它们?等等)

(因此,作为消费者,如果你的任务是遍历BinaryTree类中的所有项,并且你在数据结构101课程中跳过了该课程——如果BinaryTree编码器实现了IEnumerable——你很幸运!你不必打开一本书学习如何遍历树——只需在该对象上使用foreach语句即可完成。)

作为“生产者”(编写一个包含数据结构/集合的类),您可能不希望类的使用者处理它的结构(可能担心他们会破坏它)。因此,您可以将集合设置为私有,并仅公开一个公共IEnumerator。
这也可以实现一些统一性 - 一个集合可能有几种迭代其项的方式(PreOrder、InOrder、PostOrder、Breadth First、Depth First等),但IEnumerable只有1个实现。您可以使用它来设置迭代集合的“默认”方式。
为什么在方法中使用IEnumerable?
如果我编写一个方法,需要对集合进行迭代,并对其中的项采取行动(聚合它们?比较它们?等等),为什么要限制自己只能使用一种类型的集合?
编写以下方法public void Sum(List list) {...}以对集合中的所有项求和意味着我只能接收列表并对其求和。编写以下方法public void Sum(IEnumerable collection) {...}意味着我可以使用任何实现IEnumerable(如列表、队列、堆栈等)的对象,并对它们的所有项求和。
其他注意事项
还有延迟执行和非托管资源的问题。IEnumerable使用yield语法,这意味着您可以单独处理每个项目,并在之前和之后执行各种计算。而且,这是逐个进行的,因此您在开始时不必持有整个集合。直到枚举开始(即直到运行foreach循环)才会实际执行计算。在某些情况下,这可能非常有用且更有效率。例如,您的类可能不会在内存中保存任何集合,而是遍历某个目录中存在的所有文件、某个数据库中的项目或其他非托管资源。IEnumerable可以代替您完成这项工作(您也可以不使用IEnumerable来完成,但IEnumerable在概念上“适合”,并且它使您能够在foreach循环中使用生成的对象)。

-1

实现IEnumerable<T>通常是类表明它应该可用于“foreach”循环的首选方式,并且同一对象上的多个“foreach”循环应该独立运行。虽然除了“foreach”之外还有其他使用IEnumerable<T>的用途,但正常的指示是应该实现IEnumerable的类是其中一个,在这种类中,说“foreach foo in classItem {foo.do_something();}”是有意义的。


-1
  1. Enumerable利用延迟执行的优势,如此处所述:IEnumerable vs List - What to Use? How do they work?

  2. 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 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接