解释为什么IEnumerable比List更高效

我一直听说.net 3.5中应该使用IEnumerable而不是List,但是我找不到任何可以解释为什么它更精通的参考资料或文章。 有没有人知道解释这个的任何内容?

问这个问题的目的是为了更好地理解IEnumerable在底层做什么。 如果你能提供给我任何链接,我会做研究并发表一个答案。

IEnumerable<T>是由List<T> 实现的接口。 我怀疑你听说IEnumerable<T>应该被使用的原因是因为它是一个不太紧密的接口要求。

例如,请考虑以下方法签名:

 void Output(List<Foo> foos) { foreach(var foo in foos) { /* do something */ } } 

这个方法要求它传递一个List的具体实现。 但是这只是按顺序做的事情。 它并不需要随机访问,也不需要List<T>甚至IList<T>给出的其他内容。 相反,该方法应该接受一个IEnumerable<T>

 void Output(IEnumerable<Foo> foos) { foreach(var foo in foos) { /* do something */ } } 

现在我们使用支持我们所需操作的最一般的(最不特定的)接口。 这是面向对象devise的基本方面。 我们通过只需要我们需要的东西而减less了耦合,除此之外没有其他的东西。 我们还创build了一个更灵活的方法,因为foos参数可能是一个Queue<T> ,一个List<T>任何实现了IEnumerable<T> 。 我们不会强迫调用者将其数据结构转换为不必要的List。

因此, IEnumerable<T>并不比“性能”或“运行时”方面的列表更有效。 这就是IEnumerable<T>是一个更高效的devise构造,因为它是一个更具体的指示你的devise需要什么。 (虽然这可能会导致在特定情况下的运行时增益。)

枚举数有几个非常好的属性,当你把它们转换成列表时会丢失。 即他们:

  • 使用延迟/延迟执行
  • 是可组合的
  • 是无界的

首先我会看延期执行。 stream行测验:下面的代码会迭代input文件中的多less行?

 IEnumerable<string> ReadLines(string fileName) { using (var rdr = new StreamReader(fileName) ) { string line; while ( (line = rdr.ReadLine()) != null) yield return line; } } var SearchIDs = new int[] {1234,4321, 9802}; var lines = ReadLines("SomeFile.txt") .Where(l => l.Length > 10 && l.StartsWith("ID: ")); .Select(l => int.Parse(l.Substring(4).Trim())); .Intersect(SearchIDs); 

答案恰好是一个零。 在迭代结果之前,它实际上并没有做任何工作。 甚至在打开文件之前,您需要添加此代码:

 foreach (string line in lines) Console.WriteLine(line); 

即使在代码运行之后,它仍然只能循环一次。 比较一下你需要迭代这个代码中的几行:

 var SearchIDs = new int[] {1234,4321, 9802}; var lines = File.ReadAllLines("SomeFile.txt"); //creates a list lines = lines.Where(l => l.Length > 10 && l.StartsWith("ID: ")).ToList(); var ids = lines.Select(l => int.Parse(l.Substring(4).Trim())).ToList(); ids = ids.Intersect(SearchIDs).ToList(); foreach (string line in lines) Console.WriteLine(line); 

即使您忽略File.ReadAllLines()调用并使用第一个示例中的同一个迭代器块,第一个示例仍然会更快。 当然,你可以使用列表来编写它,但要做到这一点,需要将读取文件的代码绑定到parsing代码的代码上。 所以你失去了另一个重要的特点: 组合性

为了演示组合性,我将添加一个最后的特征 – 无界系列。 考虑以下事项:

 IEnumerable<int> Fibonacci() { int n1 = 1, n2 = 0, n; yield return 1; while (true) { n = n1 + n2; yield return n; n2 = n1; n1 = n; } } 

这看起来好像会一直持续下去,但是您可以使用IEnumerable的可组合属性来构build一些安全的function,例如,前50个值或每个小于给定数字的值:

  foreach (int f in Fibonacci().Take(50)) { /* ... */ } foreach (int f in Fibonacci().TakeWhile(i => i < 1000000) { /* ... */ } 

最后,IEnumerable更加灵活。 除非你绝对需要追加到列表或者通过索引访问项目的能力,否则几乎总是更好的编写函数来接受IEnumerables作为参数而不是列表。 为什么? 因为如果需要的话,仍然可以将列表传递给函数 – 列表 IEnumerable。 对于这个问题,数组和其他许多types的集合都是很好的。 因此,通过在这里使用IEnumerable,您可以使用完全相同的function,并使其function更强大,因为它可以处理更多不同types的数据。

IEnumerable<T>不如List<T>更有效,因为List<T> IEnumerable<T>

IEnumerable<T>接口只是.NET使用迭代器模式的方式 ,仅此而已。

这个接口可以在许多types(包含List<T> )上实现,以允许这些types返回迭代器(即IEnumerator<T>实例),以便调用者可以迭代一系列的项目。

这不是一个效率问题(尽pipe这可能是事实),而是灵活性问题。

如果代码使用IEnumerable而不是List,则代码变得更加可重用。 为了高效考虑这个代码:

  function IEnumerable<int> GetDigits() { for(int i = 0; i < 10; i++) yield return i } function int Sum(List<int> numbers) { int result = 0; foreach(int i in numbers) result += i; return i; } 

:如何获取由GetDigits生成的一组数字并将Sum加起来?
:我需要将GetDigits中的一组数字加载到List对象中,并将其传递给Sum函数。 这使用内存,因为所有的数字都需要先加载到内存中,然后再进行求和。 但是将Sum的签名更改为: –

  function int Sum(IEnumerable<int> numbers) 

意思是我可以做到这一点: –

  int sumOfDigits = Sum(GetDigits()); 

没有列表被加载到内存中我只需要存储当前数字和累加器variables。

这是两个不同的怪兽,你不能真正比较它们。 例如,在var q = from x in ...qIEnumerable ,但是在IEnumerable ,它执行一个非常昂贵的数据库调用。

IEnumerable只是Iteratordevise模式的接口,而List / IList是数据容器。

推荐使用方法返回IEnumerable<T>一个原因是它不如List<T> 。 这意味着你可以稍后改变你的方法的内部使用一些可能更有效的方法,只要它是一个IEnumerable<T>你不需要触碰你的方法的合约。

在.NET 3.5中,使用IEnumerable允许您编写具有延迟执行的方法,如下所示:

 public class MyClass { private List<int> _listOne; private List<int> _listTwo; 
public IEnumerable<int> GetItems () { foreach (int n in _listOne) { yield return n; } foreach (int n in _listTwo) { yield return n; } } }

这使您可以组合这两个列表而不创build新的List<int>对象。