给出以下代码:
var strings = Enumerable.Range(0, 100).Select(i => i.ToString());
int outValue = 0;
var someEnumerable = strings.Where(s => int.TryParse(s, out outValue))
.Select(s => outValue);
outValue = 3;
//enumerating over someEnumerable here shows ints from 0 to 99
我能够看到每次迭代的out参数的“快照”。为什么这个工作正常而不是我看到100 3(延迟执行)或100 99(访问修改后的闭包)?
答案 0 :(得分:5)
首先定义一个查询strings
,它在查询时知道如何生成字符串序列。每次要求它时,它都会生成一个新数字并将其转换为字符串。
然后您声明一个变量outValue
,并为其指定0
。
然后定义一个新查询someEnumerable
,它知道在询问值时如何从查询strings
获取下一个值,尝试解析值,如果值可以解析,产生outValue
的值。我们再一次定义了一个可以执行此操作的查询,我们不实际完成任何。
然后,您将outValue
设置为3
。
然后你问someEnumerable
它的第一个值,你要求Select
实现它的值。要计算该值,它会向Where
询问其的第一个值。 Where
会询问strings
。 (我们现在跳过几个步骤。)Where
将获得0
。它将在0
上调用谓词,专门调用int.TryParse
。这样做的副作用是outValue
将设置为0
。 TryParse
会返回true
,因此会产生该项目。 Select
然后使用其选择器将该值(字符串0
)映射到新值。选择器忽略该值并在该时间点生成outValue
的值,即0
。我们的foreach
循环现在对0
执行任何操作。
现在我们在循环的下一次迭代中询问someEnumerable
第二个值。它询问Select
一个值,Select
询问Where,
要求strings
,strings
产生"1"
,Where
调用谓词,将outValue
设置为1
作为副作用,Select
会产生outValue
的当前值,即1
。 foreach
循环现在对1
执行任何操作。
所以关键在于,由于Where
和Select
推迟执行的方式,只在需要值时立即执行工作,{{1}的副作用最终在Where
中的每个投影之前调用谓词。如果没有推迟执行,而是在Select
,中的任何投影之前执行了所有TryParse
次调用,那么你会看到每个值Select
。我们实际上可以很容易地模拟这个。我们可以将100
的结果具体化为一个集合,然后看到Where
被Select
一遍又一遍地重复的结果:
100
说完了所有这些,你所拥有的查询并不是特别好的设计。只要有可能,您应该避免有副作用的查询(例如您的var someEnumerable = strings.Where(s => int.TryParse(s, out outValue))
.ToList()//eagerly evaluate the query up to this point
.Select(s => outValue);
)。事实上,查询既会产生副作用,又会观察到它产生的副作用,这使得所有这些都变得相当困难。优选的设计是依靠纯粹的功能性方法,不会引起副作用。在此上下文中,最简单的方法是创建一个尝试解析字符串并返回Where
的方法:
int?
这允许我们写:
public static int? TryParse(string rawValue)
{
int output;
if (int.TryParse(rawValue, out output))
return output;
else
return null;
}
此处查询中没有可观察的副作用,查询也不会观察到任何外部副作用。它使整个查询更容易推理。
答案 1 :(得分:1)
因为当您枚举值时,一次更改一个值并动态更改变量的值。由于LINQ的性质,第一次迭代的选择在第二次迭代的位置之前执行。基本上这个变量变成了一种foreach循环变量。
这是延迟执行购买我们的东西。以前的方法不必在链中的下一个方法开始之前完全执行。在第二个进入之前,一个值会遍历所有方法。这对于像First或Take这样的方法非常有用,它们可以提前停止迭代。规则的例外是需要像OrderBy一样聚合或排序的方法(他们需要在找出哪个元素之前查看所有元素)。如果在选择之前添加OrderBy,行为可能会中断。
当然,我不会在生产代码中依赖此行为。
答案 2 :(得分:0)
我不明白你有多奇怪?
如果你像这样写这个可枚举的循环
foreach (var i in someEnumerable)
{
Console.WriteLine(outValue);
}
因为LINQ枚举每个位置并且懒惰地选择并产生每个值,所以如果添加ToArray
var someEnumerable = strings.Where(s => int.TryParse(s, out outValue))
.Select(s => outValue).ToArray();
比在循环中你会看到99秒
修改强>
以下代码将打印99秒
var strings = Enumerable.Range(0, 100).Select(i => i.ToString());
int outValue = 0;
var someEnumerable = strings.Where(s => int.TryParse(s, out outValue))
.Select(s => outValue).ToArray();
//outValue = 3;
foreach (var i in someEnumerable)
{
Console.WriteLine(outValue);
}