解释为什么IEnumerable比List更有效

时间:2009-08-28 13:56:41

标签: generics .net-3.5

我一直听说.net 3.5你应该在List上使用IEnumerable,但我找不到任何参考资料或文章来解释为什么它更加精通。有谁知道解释这个的任何内容?

提出这个问题的目的是为了更好地理解IEnumerable在幕后做的事情。如果您能为我提供任何链接,我将进行研究并发布答案。

7 个答案:

答案 0 :(得分:67)

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 */ }
}

现在我们正在使用支持我们所需操作的最通用(最不具体)接口。这是OO设计的一个基本方面。我们通过只需要我们需要的东西来减少耦合,而不仅仅需要其他东西。我们还创建了一个更加灵活的方法,因为foos参数可能是Queue<T>List<T>任何实现IEnumerable<T>。我们并没有强制调用者不必要地将他们的数据结构转换为List。

因此,IEnumerable<T>比“性能”或“运行时”方面的列表更有效。 IEnumerable<T>是一种更高效的设计构造,因为它更能说明您的设计需求。 (虽然这可以在特定情况下导致运行时增益。)

答案 1 :(得分:40)

Enumerables有几个非常好的属性,在将它们转换为列表时会丢失。即他们:

  • 使用延迟/延迟执行
  • 可以合成
  • 无界限

首先,我将介绍延迟执行。弹出测验:以下代码将多少次迭代输入文件中的行?

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()调用并使用第一个样本中的相同迭代器块,第一个样本仍然会更快。当然,您可以将其编写为使用列表一样快,但要做到这一点,需要将读取文件的代码绑定到代码中解析它的代码。所以你失去了另一个重要的功能:可组合性

为了展示可组合性,我将添加一个最终功能 - 无界系列。考虑以下内容:

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的可组合性属性来构建安全地给出前50个值的东西,或者每个小于给定数字的值:

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

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

答案 2 :(得分:5)

由于IEnumerable<T> List<T>

List<T>效率不高于IEnumerable<T>

IEnumerable<T>界面只是.NET使用iterator pattern的方式,仅此而已。

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

答案 3 :(得分:3)

这不是效率问题(尽管可能是真的),而是灵活性。

如果代码可以使用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来添加它们?
A :我需要将GetDigits中的数字集加载到List对象中,并将其传递给Sum函数。这使用内存,因为所有数字需要先加载到内存中才能求和。但是将Sum的签名更改为: -

 function int Sum(IEnumerable<int> numbers)

意味着我可以这样做: -

 int sumOfDigits = Sum(GetDigits());

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

答案 4 :(得分:1)

这是两种不同的野兽,你无法真正比​​较它们。例如,在var q = from x in ...中,qIEnumerable,但在幕后,它会执行非常昂贵的数据库调用。

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

答案 5 :(得分:1)

建议让方法返回IEnumerable<T>的一个原因是它没有List<T>那么具体。这意味着您可以稍后更改方法的内部,以使用可能更有效的方法来满足需求,只要它是IEnumerable<T>,您就不需要触及方法的契约。

答案 6 :(得分:0)

在.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; } } }

这允许您在不创建新List<int>对象的情况下组合两个列表。