您的 LINQ 查询不会返回所请求的数据,它返回的是获取可以逐个访问数据元素的东西的可能性。
在软件方面:您的 LINQ 语句的值是 IEnumerable<T>(或 IQueryable<T>,此处不再讨论)。此对象不保存您的数据。
事实上,你不能用IEnumerable<T> 做很多事情。它唯一能做的就是生成另一个实现IEnumerator<T> 的对象。 (注意区别:IEnumerable vs IEnumerator)。这个 `GetEnumerator()' 函数是我第一句话中的“get something that can access ...”部分。
您从IEnumerable<T>.GetEnumerator() 获得的对象实现了 IEnumerator。该对象也不必保存您的数据。它只知道如何生成数据的第一个元素(如果有的话),如果它有一个元素,它知道如何获取下一个元素(如果有的话)。这是我第一句话中的“可以逐个访问数据的元素”。
所以IEnumerable<T> 和Enumerator<T> 都不需要(必须)保存您的数据。它们只是帮助您以定义的顺序访问数据的对象。
在早期,当我们没有List<T> 或实现IEnumerable<T> 的类似集合类时,实现IEnumerable<T> 和IEnumerator<T> 函数Reset、Current 是相当麻烦的和MoveNext。事实上,现在很难找到不使用同样实现IEnumerator<T> 的类来实现IEnumerator<T> 的示例。 Example
关键字Yield 的引入大大简化了IEnumerable<T> 和IEnumerator<T> 的实现。如果函数包含Yield return,则返回IEnumerable<T>:
IEnumerable<double> GetMySpecialNumbers()
{ // returns the sequence: 0, 1, pi and e
yield return 0.0;
yield return 1.0;
yield return 4.0 * Math.Atan(1.0);
yield return Math.Log(1.0)
}
请注意,我使用术语序列。它不是List,也不是Dictionary,只能通过请求第一个来访问元素,并反复请求下一个。
您可以使用IEnumerable<T>.GetEnumerator() 和IEnumerator<T> 的三个函数访问序列的元素。这种方法已经很少使用了:
IEnumerable<double> myNumbers = GetMySpecialNumbers();
IEnumerator<double> enumerator = myNumbers.GetEnumerator();
enumerator.Reset();
// while there are numbers, write the next one
while(enumerator.MoveNext())
{ // there is still an element in the sequence
double valueToWrite = enumerator.Current();
Console.WriteLine(valueToWrite);
}
随着foreach 的引入,这变得容易多了:
foreach (double valueToWrite in GetMySpecialNumbers())
Console.WriteLine(valueToWrite);
在内部这将执行 GetNumerator() 和 Reset() / MoveNext() / Current()
所有通用集合类,如 List、Array、Dictionary、HashTable 等,都实现了 IEnumerable。大多数情况下,函数返回一个 IEnumerable,您会发现它在内部使用这些集合类之一。
继yield 和foreach 之后的另一项伟大发明是引入了扩展方法。见extension methods demystified。
扩展方法使您能够获取一个您无法更改的类,例如List<T>,并为它编写新功能,只使用您有权访问的功能。
这是 LINQ 的提升。它使我们能够为以下所有内容编写新功能:“嘿,我是一个序列,你可以要求我的第一个元素和我的下一个元素”(= 我实现了 IEnumerable)。
如果您查看source code of LINQ,,您会发现诸如 Where / Select / First / Reverse / ... 等 LINQ 函数被编写为 IEnumerable 的扩展函数。它们中的大多数使用通用集合类(HashTable、Dictionary),其中一些使用yield return,有时您甚至会看到基本的IEnumerator 函数,如Reset / MoveNext
您通常会通过连接 LINQ 函数来编写新功能。但是请记住,有时yield 会使您的函数更易于理解,从而更易于重用、调试和维护。
示例:假设您有一系列生成的Products。每个Product 都有一个DateTime 属性ProductCompletedTime,表示其产品生产完成的时间。
假设您想知道两个完成的产品之间有多少时间。
问题:无法为第一个产品计算。
有了收益,这很容易:
public static IEnumerable<TimeSpan> ToProductionTimes<Product>
(this IEnumerable<Product> products)
{
var orderedProducts = product.OrderBy(product => product.ProductionTime;
Product previousProduct = orderedProducts.FirstOrDefault();
foreach (Product product in orderedProducts.Skip(1))
{
yield return product.ProductCompletedTime - previouseProduct.ProductCompletedTime;
previousProduct = product;
}
}
尝试在 Linq 中执行此操作,会更难理解会发生什么。
结论
一个 IEnumerable 不保存您的数据,它只保存一个一个访问您的数据的潜力。
访问数据最常用的方法有foreach、ToList()、ToDictionary、First等。
每当您需要编写一个返回困难IEnumerable<T> 的函数时,至少考虑编写一个yield return 函数。