惰性求值
理论
定义查询操作时,程序并不会立刻把数据获取过来并填充到序列中,因为你定义的实际上只是一套执行步骤而已,等真正需要遍历查询结果时,才会得以执行。也就是说,对查询结果做迭代的时候,程序总是会从头开始执行这套步骤,这样做通常是合理的。每迭代一遍都产生一套新的结果,这叫作 惰性求值(lazy evaluation) ,反之,如果像编写普通的代码那样直接查询某一套变量的取值并将其立刻记录下来,那么就称为 及早求值(eager evaluation) .
如果你要定义的查询操作需要多次执行,那么就得考虑到底应该采用哪种求值方式才好你是想给数据做一份快照,近是想先把食间逻辑描述出来,以便将来能够随时根据这套逻辑来获取查询结果并将其放人序列中?
惰性求值与编写普通代码时所用的思路有很大区别,因为你在编写其他代码的时候可能会理所当然地认为,那些代码就是应该立刻得到执行才对。但是LINQ查询操作与那些代码不同,它会把代码当成数据来看,用作参数的lambda表达式要等到以后再去调用(而不是立刻就得以执行)。此外,如果provider使用的是 表达式树(expession te) 而不是委托,那么稍后可能还会有新的表达式融人这棵树中。
例子
由于查询表达式可以惰性求值,因此,从理论来说,能够用来操作 无穷序列(Infinite sequence) 。如果代码写得较为合理,那么程序只需要检查序列的开头部分即可,因为它可以在找到所需的答案时停下来。反之,有些写法则会令查询表达式必须把整个序列处理一遍才能得出完整的结果。开发者需要理解这两种情况,以编写出可以流畅执行的查询语句,并避开瓶颈,以防写出那种必须把整个序列处理一遍才能求出结果的代码。
public static void Main(string[] args)
{
var answers = from number in AllNumber() select number;
var smallNumber = answers.Take(10);
foreach (var i in smallNumber)
{
Console.WriteLine(i);
}
}
static IEnumerable<int> AllNumber()
{
var number = 0;
while (number < int.MaxValue)
{
yield return number++;
}
}
![]()
这个例子演示了刚才所说的第一种情况。 这种情况下,不需要把整个序列生成出来Main方法所打印出来的是 0,1,2,3,4,5,6,7,8,9 这十个数字。就 AlINumbers() 方法本身来说,它可以一直生成下去 (当然,AllNumbers() 还是会在 number 变量达到 int.MaxValue 时停止生成,但你应该没有耐心等到那个时候),但在本例中,它只生成十个数。之所以不用把整个序列全都生成出来,是因为 Take() 方法只需要其中的前 N 个对象,而不关心后面那些对象。
其他
反之,如果把查询语句改成下面这样,那么程序就会一直运行下去:
public static void Main(string[] args)
{
var answers = from number in AllNumber()
where number < 10
select number;
var smallNumber = answers.Take(10);
foreach (var i in smallNumber)
{
Console.WriteLine(i);
}
}
static IEnumerable<int> AllNumber()
{
var number = 0;
while (number < int.MaxValue)
{
yield return number++;
}
}
程序必须运行到 number 变量等于 int.Maxvalue 时才会停下,因为查询语句需要逐个判断序列中的每一个元索,并根据其是否小于10来决定要不要生成该元素。这样的逻辑导致它必须把整个序列全都处理一遍才行。
某些查询操作必须把整个序列处理遍,然后才能得出正确结果。比方说,刚才那个例子里面的 Where 就会导致这样的情况发生,因为它需要判断源序列中的每-个元素,而且可能会产生另一个无穷序列。此外还有 OrderBy ,它必须知道整个序列的内容,才能够完成排序,而 Max 与 Min 也需要知道整个序列的内容,才能决定最大值与最小值。这些操作无法只根据序列中的某一部分内容而执行, 因此, 在用到这些功能时, 需要处理整个序列。
参考
- Effective C# (Third Edition)