LINQ之延迟加载及其原理
这是LINQ(集成化查询)的继续及补充,在前面我已经介绍过,在LINQ中,一个重要的特性就是延迟加载,是指查询操作并不是在查询运算符定义的时候执行,而是在真正使用集合中的数据时才执行(如:在遍历集合时调用MoveNext方法时)。下面是一个简单的实例:
var num = new List<int>(); num.Add(1); IEnumerable<int> query = num.Select(n => n * 10); num.Add(2); foreach (int n in query) Console.WriteLine(n);
结果输出10 20.在上面的例子中,在创建查询语句后又向集合中加入新元素,这个新元素最终也出现在查询结果中。这就是因为查询语句是在遇到foreach之后才真正执行的,再例如:
Action a = () => Console.WriteLine("Foo"); //没有在控制台输出任何内容 a(); //延迟执行,输出Foo
绝大部分标准的LINQ查询运算符都具有延迟加载这种特性,但也有例外:
- 那些返回单个元素或返回一个数值的运算符,如First或Count。
- 转换运算符:ToArray,ToList,ToDictonnary,ToLookup。
以上这些运算符都会触发LINQ语句立即执行,因为它们的返回值类型不支持延迟加载。
延迟加载的工作原理
LINQ查询运算符之所以有延迟加载功能,是因为每个返回值不是一般的数组或集合,而是一个经过封装的序列,这种序列通常情况下并不直接储存数据元素,它封装并使用运行时传递给它的集合,元素也由其他集合来储存,它实际上只是维护自己与数据集合的一种依赖关系,当有查询请求时,再到它以来的序列中进行真正的查询。查询运算符实际上是封装一系列的转换函数,这种转换函数可以将与之关联的数据转换为各种形式的序列。如果输出集合不需要转换的话,那么就不用执行查询运算符封装的转换操作,这个时候查询运算符实际上就是一个委托,进行数据转发而已。
例如调用Where运算符的时候,在Where内部所做的操作非常简单,根据Lambda表达式中指定的查询条件,对输入集合重新进行了筛选,保留那些符合条件的元素指针引用,当外部遍历Where的返回值时,Where回到它所关联的集合中进行真正的查询,然后返回查询结果。
IEnumerable<int> lessThanTen = new int[] { 5, 12, 3 }.Where(n => n < 10);
当执行lessThanTen操作时,实际上,就是使用where运算符对封装序列进行筛选。
当需要使用一些有特殊功能的运算符时,我们可以自定义实现。例如下面这个实例所示,实现一个自定义的Select运算符:
public static IEnumerable<TResult> Select<TSource,TResult>( this IEnumerable<TSource> source,Func<TSource,TResult> selector) { foreach (TSource element in source) yield return selector(element); }
上面这个方法实际上就是在循环中返回yield类型的元素。所以当调用Select和Where查询运算符时,内部操作就是创建一个序列,然后将查询得到的元素存入这个新的序列中。
其实,在LINQ中,延迟加载特性是很重要的,这种设计将查询的转换和查询的执行进行了解耦,这使得我们可以将查询分成多个步骤来创建,有利于查询表达式的书写,而且在执行的时候按照一个完整的结构去查询,减少了对集合的查询次数,这种特性对数据库查询尤其重要。