关于c#:IEnumerable< T>的作用是什么?

What's the role of IEnumerable<T> and why should I use it?

我为什么要用IEnumerable来处理…比如说List呢?前者比后者有什么优势?


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

另一方面,ListIEnumerable的特定实现,它以特定的已知方式存储对象。在内部,这可能是存储通过IEnumerable公开的值的非常好的方法,但List并不总是合适的。例如,如果您不需要按索引访问项,而是在集合的开始处不断插入项,然后从结尾处删除项,那么使用Queue将更为合适。

通过在API中使用IEnumerable,您可以随时灵活地更改内部实现,而不必更改任何其他代码。这在允许您的代码具有灵活性和可维护性方面有巨大的好处。


在这一点上,杰弗里·里克特写道:

在声明方法的参数类型时,应指定尽可能最弱的类型,首选接口而不是基类。例如,如果编写的方法操作项集合,最好通过使用IEnumerable等接口,而不是使用List等强数据类型,甚至使用ICollectionIList等强接口类型:

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的对象。

另一方面,通常最好使用尽可能强的类型来声明方法的返回类型(尽量不要将自己提交到特定的类型)。


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

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

第一个例子是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按索引访问对象。

这里有一个很好的指导方针,在什么时候使用IEnumerableICollectionList等等。


通常不直接使用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();"。