我一直听说在 .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 */ }
}
现在我们使用的是支持我们需要操作的最一般(最不具体)接口。这是面向对象设计的基本特征。我们通过仅要求所需内容而不是其他不必要的内容来降低耦合度。同时,我们创建了一种更加灵活的方法,因为foos
参数可以是Queue<T>
、List<T>
或实现IEnumerable<T>
接口的任何类型。我们不会强制调用者不必要地将它们的数据结构转换为List。
因此,IEnumerable<T>
并不比List在“性能”或“运行时”方面更有效率。而是因为IEnumerable<T>
是一个更具体的指示,更符合你的设计要求,所以是一种更有效率的设计构造。(尽管在特定情况下,这可能会导致运行时收益。)
IEnumerable<T>
的方法延迟执行效率的重要点。 - jeremyalanIEnumerable<T>
的方法都使用延迟执行(可能这就是为什么原始回答提到“这可能会导致特定情况下的运行时增益”), - Jeff SternalIList<T>
,或者在任何基类的自己的扩展中实现延迟执行。 - yfeldblum当将可枚举对象转换为列表时,会失去它们几个非常好的特性。其中包括:
首先我将介绍延迟执行。问题来了:以下代码将对输入文件中的行进行多少次迭代?
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更加灵活。除非您绝对需要向列表追加元素或按索引访问项目,否则编写函数以接受IEnumerable作为参数几乎总是比编写函数以接受List更好。为什么呢?因为如果需要的话,您仍然可以将列表传递给该函数 - List 就是一个IEnumerable。同样,数组和许多其他集合类型也是如此。因此,通过在此处使用IEnumerable,您可以将完全相同的函数变得更加强大,因为它可以处理更多不同类型的数据。
IEnumerable<T>
并不比List<T>
更高效,因为List<T>
本身就是一个IEnumerable<T>
。
IEnumerable<T>
接口只是.NET使用迭代器模式的一种方式,没有任何其他作用。
这个接口可以在很多类型(包括List<T>
)上实现,以允许这些类型返回迭代器(即IEnumerator<T>
实例),以便调用者可以迭代一系列项目。
IEnumerable<T>
实现,而无需创建任何实例,并支持昂贵操作的延迟执行。 - jeremyalanList<T>
作为IEnumerable<T>
的实现者。还是我误解了你的评论? - Greg D这不是效率的问题(虽然这可能是真的),而是灵活性的问题。
如果您的代码可以消耗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生成的数字集中获取它们的总和?
答: 我需要将GetDigits生成的数字集加载到List对象中,并将其传递给Sum函数。这会使用内存,因为所有数字都需要首先加载到内存中,然后才能进行求和。但是,可以通过更改Sum函数的签名来解决此问题:-
function int Sum(IEnumerable<int> numbers)
这意味着我可以做到:
int sumOfDigits = Sum(GetDigits());
没有将列表加载到内存中,我只需要为当前数字和累加器变量在总和中提供存储空间。
这是两个不同的东西,你不能真正地比较它们。例如,在 var q = from x in ...
中,q
是 IEnumerable
,但在底层它执行了一个非常昂贵的数据库调用。
IEnumerable
只是迭代器设计模式的接口,而 List
/IList
是一个数据容器。
public class MyClass
{
private <code>List<int></code> _listOne;
private <code>List<int></code> _listTwo;<br>
public <code>IEnumerable<int></code>
GetItems ()
{
foreach (int n in _listOne)
{
yield return n;
}
foreach (int n in _listTwo)
{
yield return n;
}
}
}
这样可以让你将两个列表合并而不需要创建一个新的List<int>
对象。
List<int>
,但是你允许编译器代表你创建一个状态机来使这一切工作。 - Andrew Hare