最近我遇到了这样的问题:
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是闭包)之后,因此使用新值.
当您设置numbers到null,你设置参考不通,但对象仍然存在.将IEnumerable通过返回Where(在参考query)仍然引用它,没关系,最初的参考是null现在.
这解释了行为:numbers并threshold在延迟执行中扮演不同的角色.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的哪种风格,并且对其内部工作的某些理解是一个优势.