我一直听说.net 3.5你应该在List上使用IEnumerable,但我找不到任何参考资料或文章来解释为什么它更加精通。有谁知道解释这个的任何内容?
提出这个问题的目的是为了更好地理解IEnumerable在幕后做的事情。如果您能为我提供任何链接,我将进行研究并发布答案。
答案 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 ...
中,q
是IEnumerable
,但在幕后,它会执行非常昂贵的数据库调用。
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>
对象的情况下组合两个列表。