为什么我的LINQ表达式中的这个输出变量没有问题
本文关键字:输出 变量 有问题 我的 LINQ 表达式 为什么 | 更新日期: 2023-09-27 18:28:26
给定以下代码:
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(访问修改的闭包)?
首先定义一个查询strings
,它知道如何在查询时生成字符串序列。每次请求值时,它都会生成一个新的数字并将其转换为字符串。
然后声明一个变量outValue
,并将0
分配给它
然后定义一个新的查询someEnumerable
,该查询知道如何在被要求获取值时从查询strings
中获取下一个值,尝试解析该值,如果该值可以解析,则生成outValue
的值。再一次,我们定义了一个可以执行此操作的查询,我们没有实际执行任何。
然后将outValue
设置为3
。
然后你问someEnumerable
它的第一个值,你问的是Select
的实现它的值。为了计算该值,它将向Where
请求其的第一个值。Where
将询问strings
。(我们现在跳过几个步骤。)Where
将获得一个0
。它将调用0
上的谓词,特别是调用int.TryParse
。这样做的副作用是outValue
将被设置为0
。TryParse
返回true
,则产生该项。CCD_ 22然后使用其选择器将该值(字符串0
)映射为新值。选择器忽略该值,并在该时间点产生outValue
的值,即0
。我们的foreach
循环现在对0
执行任何操作。
现在我们在循环的下一次迭代中询问someEnumerable
的第二个值。它向Select
请求一个值,Select
向Where,
请求strings
,strings
产生"1"
,Where
调用谓词,将outValue
设置为1
作为副作用,Select
产生outValue
的当前值,即1
。foreach
循环现在对1
执行任何操作。
因此,这里的关键点是,由于Where
和Select
延迟执行的方式,只有在需要值时才立即执行它们的工作,因此Where
谓词的副作用最终会在Select
中的每个投影之前立即调用。如果没有延迟执行,而是在Select
中的任何投影之前执行所有TryParse
调用,则您将看到每个值的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;
}
这允许我们写:
var someEnumerable = from s in strings
let n = TryParse(s)
where n != null
select n.Value;
在这里,查询中没有可观察到的副作用,查询也没有观察到任何外部副作用。它使整个查询更容易推理。
因为当您枚举时,值一次更改一个,并动态更改变量的值。由于LINQ的性质,第一次迭代的选择在第二次迭代的where之前执行。基本上,这个变量变成了一个foreach循环变量。
这就是延迟执行为我们带来的好处。在链中的下一个方法开始之前,以前的方法不必完全执行。一个值在第二个值进入之前遍历所有方法。这对于像First或Take这样提前停止迭代的方法非常有用。该规则的例外是需要像OrderBy那样进行聚合或排序的方法(它们需要查看所有元素,然后才能找出哪个是第一个)。如果在Select之前添加OrderBy,则行为可能会中断。
当然,我不会依赖于生产代码中的这种行为。
我不明白什么对你来说奇怪?
如果你在这个像这样的可枚举对象上写一个循环
foreach (var i in someEnumerable)
{
Console.WriteLine(outValue);
}
因为LINQ枚举了每个where并懒洋洋地选择并产生了每个值,所以如果您添加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);
}