延迟LINQ查询执行实际上如何工作?

Mic*_*zyk 26 c# linq

最近我遇到了这样的问题: What numbers will be printed considering the following code:

class Program
{
    static void Main(string[] args)
    {
        int[] numbers = { 1, 3, 5, 7, 9 };
        int threshold = 6;
        var query = from value in numbers where value >= threshold select value;

        threshold = 3;
        var result = query.ToList();

        result.ForEach(Console.WriteLine);
        Console.ReadLine();
    }
}
Run Code Online (Sandbox Code Playgroud)

回答: 3, 5, 7, 9

这让我很惊讶.我认为该threshold值将在查询构造中放入堆栈,稍后在执行时,该数字将被拉回并在条件中使用..这种情况没有发生.

另一种情况(在执行之前numbers设置null):

    static void Main(string[] args)
    {
        int[] numbers = { 1, 3, 5, 7, 9 };
        int threshold = 6;
        var query = from value in numbers where value >= threshold select value;

        threshold = 3;
        numbers = null;
        var result = query.ToList();
        ...
    }
Run Code Online (Sandbox Code Playgroud)

似乎对查询没有影响.它打印出与前一个示例完全相同的答案.

谁能帮助我了解幕后真的发生了什么?为什么更改threshold对查询执行有影响,而更改numbers却没有?

Sef*_*efe 26

您的查询可以像方法语法一样编写:

var query = numbers.Where(value => value >= threshold);
Run Code Online (Sandbox Code Playgroud)

要么:

Func<int, bool> predicate = delegate(value) {
    return value >= threshold;
}
IEnumerable<int> query = numbers.Where(predicate);
Run Code Online (Sandbox Code Playgroud)

这些代码片段(包括您在查询语法中的查询)都是等效的.

当您像这样展开查询时,您会看到这predicate是一个匿名方法,并且threshold是该方法中的闭包.这意味着它将在执行时采用该值.编译器将生成一个实际(非匿名)方法来处理它.声明时不会执行该方法,但query枚举时的每个项目都会执行(执行被延迟).由于枚举发生在threshold更改值(并且threshold是闭包)之后,因此使用新值.

当您设置numbersnull,你设置参考不通,但对象仍然存在.将IEnumerable通过返回Where(在参考query)仍然引用它,没关系,最初的参考是null现在.

这解释了行为:numbersthreshold在延迟执行中扮演不同的角色.numbers是对枚举的数组的引用,threshold而是一个局部变量,其范围被"转发"到匿名方法.

扩展,第1部分:在枚举期间修改封闭

当您更换线路时,您可以更进一步示例...

var result = query.ToList();
Run Code Online (Sandbox Code Playgroud)

...有:

List<int> result = new List<int>();
foreach(int value in query) {
    threshold = 8;
    result.Add(value);
}
Run Code Online (Sandbox Code Playgroud)

您正在做的是更改数组迭代threshold 期间的值.当您第一次点击循环体(当value为3时)时,将阈值更改为8,这意味着将跳过值5和7,并且要添加到列表中的下一个值为9.原因是threshold将在每次迭代时再次评估值,然后使用当时有效的值.并且由于阈值已经变为8,因此数字5和7不再评估为大于或等于.

扩展,第2部分:实体框架是不同的

为了使事情变得更复杂,当您使用LINQ提供程序创建与原始查询不同的查询然后执行它时,情况会略有不同.最常见的例子是实体框架(EF)和LINQ2SQL(现在很大程度上被EF取代).这些提供程序在枚举之前从原始查询创建SQL查询.从那时起,闭包的值只被评估一次(它实际上不是闭包,因为编译器生成表达式树而不是匿名方法),threshold枚举期间的更改对结果没有影响.在将查询提交到数据库之后会发生这些更改.

从中得到的教训是,您必须始终了解您使用的LINQ的哪种风格,并且对其内部工作的某些理解是一个优势.