LINQ学习笔记:查询是怎么执行的
延迟执行
对于多数的查询操作符来说, 他们并不是在构造后被立即执行, 而是当枚举发生的时候, 换句话说就是当它对应的枚举器上的MoveNext被调用的时候. 例如下面的查询:
1: var numbers = new List<int>( );
2: numbers.Add (3);
3:
4: IEnumerable<int> q = numbers.Select (n => n + 2);
5: numbers.Add (5);
6:
7: foreach (int n in q)
8: Console.Write (n + ","); //5,7,
可以发现我们在查询构造之后插入的数字也包含在结果之中, 因为只有当foreach表达式运行的时候过滤或者排序才会发生. 我们把它称为延迟执行. 几乎所有的标准查询操作符都具有延迟执行的能力, 但以下这些是例外:
1. 那些返回单一元素或者单一值得操作符, 例如First或者Count
2. 转换操作符: ToArray, ToList, ToDictionary, ToLookup
这些操作符会被立即执行因为他们的返回类型没有任何的机制来提供延迟执行. 例如Count, 返回一个简单的整数类型, 没有办法被枚举. 例如下面的查询会被立即执行:
1: int result = numbers.Where (n => n <6).Count();
2: Console.WriteLine(result); //2
延迟执行非常重要是因为它将查询构造和执行解耦了. 这允许你可以分几步构造一个查询, 并使LINQ to SQL变得可能.
重估值
延迟执行还带来另外一个后果, 就是当你重新枚举的时候延迟执行的查询将会被重新计算.
1: var numbers = new List<int>( ) { 5, 6 };
2:
3: IEnumerable<int> q = numbers.Select (n => n + 10);
4: foreach (int n in q)
5: Console.Write (n + ","); // 15,16,
6:
7: numbers.Clear( );
8: foreach (int n in q)
9: Console.Write (n + ","); // nothing
有几个理由可以解释为什么重估有些时候会带来一些不利的影响:
1. 有些时候你想在一个特定的点及时冻结或者缓存结果
2. 有些查询是密集计算(或者依赖于一个远程数据库), 因此我们不想做一些不必要的重复.
要避免重估我们可以调用一个转换操作符, 例如ToArray或者ToList. ToArray将一个输出序列拷贝到一个数组, ToList则是将其拷贝到一个泛型的List<>:
1: var numbers = new List<int>( ) { 5, 6, 7 };
2:
3: List<int> r = numbers
4: .Select (n => n + 10)
5: .ToList( );
6:
7: numbers.Clear( );
8: Console.WriteLine (r.Count); //3
外部变量
如果查询语句中的Lambda表达式引用了本地变量, 这些变量的值将是查询被执行时候的值, 而不是第一次被捕获时候的值, 例如:
1: int[] numbers = { 10, 20 };
2: int factor = 10;
3: var query = numbers.Select (n => n * factor);
4: factor = 20; // Change value
5: foreach (int n in query)
6: Console.Write (n + ","); // 200,400,
当我们使用一个foreach循环创建一个查询的时候这可能会是一个陷阱, 例如:
1: IEnumerable<char> query = "Not what you might expect";
2: foreach (char vowel in "aeiou")
3: {
4: char temp = vowel;
5: query = query.Where (c => c != temp); //如果使用vowel, 那么将只有’u’会被删除
6: }
延迟执行是如何工作的?
查询操作符通过返回装饰过的序列来提供延迟执行功能.
与传统的集合类( 例如Array或者Linked List)不同的是, 一个装饰过的序列本身并没有自己的数据结构来存储元素, 相反的, 它包装了你在运行时提供的序列, 因此, 它保持了一个永久的依赖. 任何时候你从装饰器中请求数据, 它都会将请求转发到包装过的输入序列中去.
调用Where操作符的时候只是构造了装饰序列, 并保持了输入序列, Lambda表达式和其他参数的引用. 只有当装饰器被枚举的时候输入序列才会被枚举.
例如:
1: IEnumerable<int> lessThanTen =
2: new int[] { 5, 12, 3 }.Where (n => n < 10);
当你枚举lessThanTen的时候, 你实际上是在通过where装饰器查询数组. 一个好消息是你可以通过一个C#迭代器来实现装饰序列从而编写你自己的查询操作符. 以下演示了怎么编写你自己的Select方法:
1: static IEnumerable<TResult> Select<TSource,TResult> (
2: this IEnumerable<TSource> source,
3: Func<TSource,TResult> selector)
4: {
5: foreach (TSource element in source)
6: yield return selector (element);
7: }
此方法是利用了yield return表达式的优点返回了一个迭代器, 以下是一个相同表达的快捷版本:
1: static IEnumerable<TResult> Select<TSource,TResult> (
2: this IEnumerable<TSource> source,
3: Func<TSource,TResult> selector)
4: {
5: return new SelectSequence (source, selector);
6: }
在这里, SelectSequence是一个有编译器生成的类型,其枚举器将逻辑装入迭代方法中.因此, 当你调用一个操作符例如Select或者Where的时候, 你实际上仅仅是实例化了一个包装了输入序列的可枚举类型, 仅此而已.
链式装饰器
连接多个查询操作符可以用来创建一个多层的装饰器. 考虑下面的查询:
1: IEnumerable<int> query = new int[] { 5, 12, 3 }
2: .Where (n => n < 10)
3: .OrderBy (n => n)
4: .Select (n => n * 10);
每一个查询操作符实例化了一个新的装饰器,这个装饰器又包装了之前的序列. 当你枚举query的时候, 你是在查询最初的数组, 并让其穿过一个多层的链式装饰器来完成转换.
另外, 延迟执行的一个特性就是如果你使用渐进式的方式组建你的查询, 也可以创建一个完全等同的对象模型:
1: IEnumerable<int>
2: source = new int[] { 5, 12, 3 },
3: filtered = source .Where (n => n < 10),
4: sorted = filtered .OrderBy (n => n),
5: query = sorted .Select (n => n * 10);
查询是怎么执行的?
这是枚举之前的查询得到的结果:
foreach (int n in query)
Console.Write (n + ","); // 30,50,
在幕后, foreach调用了Select装饰器上的GetEnumberator(最外面的操作符),然后开始触发所有的事件. 结果就是一个链式枚举器结构上对应到一个链式的装饰器序列.
一个查询就是一个生产线的传送带, 我们可以说一个LINQ查询就是一个懒惰的生产线, 传送带和lambda工人根据需求来生产各个元素. 构造一个查询就是在构造一个生产线 – 我们具备了所有的一切条件– 但还没有生产任何的东西. 当消费者请求一个元素的时候(枚举查询结果), 最右边的传送带激活, 然后触发其他的传送带开始生产 –这时需要提供一个输入序列. LINQ使用了一个需求驱动的拉模型, 而不是一个供应驱动的推模型. 这对于LINQ to SQL是非常重要的. 待续!