11.1.4 推迟查询的执行
在运行期间定义查询表达式时,查询就不会运行,查询会在迭代数据项时运行。
比如说扩展方法Where()。它使用yield return语句返回谓词为true的元素。因为使用了yield return语句,所以编译器会创建一个枚举器,在访问枚举中的项后,就返回它们。
public static IEnumerable<T> Where<T>(this IEnumerable<T> Source, Func<T, bool> predicate) { foreach(T item in Source) { if(predicate(item)) yield return item; } }
这是一个非常有趣也非常重要的结果。在下面的例子中,创建了String元素的一个集合,用名称arr填充它。接着定义一个查询,从集合中找出以字母J开头的所有名称。集合也应是排序好的。在定义查询时,不会进行迭代。相反,迭代在foreach语句中进行,在其中迭代所有的项。集合中只有一个元素Juan满足where表达式的要求,即以字母J开头。迭代完成后,将Juan写入控制台。之后在集合中添加4个新名称,再次进行迭代。
var names = new List<string> { "Nino", "Alberto", "Juan", "Mike", "Phil" }; var namesWithJ = from n in names where n.StartsWith("J") orderby n select n; Console.WriteLine("First iteration"); foreach(string name in namesWithJ) { Console.WriteLine(name); } Console.WriteLine(); names.Add("John"); names.Add("Jim"); names.Add("Jack"); names.Add("Denny"); Console.WriteLine("Second iteration"); foreach(string name in namesWithJ) { Console.WriteLine(name); }
因为迭代在查询定义时不会进行,而是在执行每个foreach语句时进行,所以可以看到其中的变化,如应用程序的结果所示:
First iteration
Juan
Second iteration
Jack
Jim
John
Juan
当然,还必须注意,每次在迭代中使用查询时,都会调用扩展方法。在大多数情况下,这是非常有效的,因为我们可以检测出数据源中的变化。但是在一些情况下,这是不可行的。调用扩展方法ToArray()、ToEnumerable()、ToList()等可以改变这个操作,在示例中,ToList遍历集合,返回一个实现了Ilist<string>的集合。之后对返回的列表遍历两次,在两次迭代之间,数据源得到了新名称。
var names = new List<string> { "Nino", "Alberto", "Juan", "Mike", "Phil" }; var namesWithJ = (from n in names where n.StartsWith("J") orderby n select n).ToList(); Console.WriteLine("First iteration"); foreach(string name in namesWithJ) { Console.WriteLine(name); } Console.WriteLine(); names.Add("John"); names.Add("Jim"); names.Add("Jack"); names.Add("Denny"); Console.WriteLine("Second iteration"); foreach(string name in namesWithJ) { Console.WriteLine(name); }
在结果中可以看到,在两次迭代之间输出保持不变,但集合中的值改变了:
First iteration
Juan
Second iteration
Juan
——摘自 《C#高级编程(第七版)》 第189页