为什么我的LINQ表达式中的输出变量没有问题?

时间:2014-10-30 20:07:37

标签: c# .net vb.net linq

给出以下代码:

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(访问修改后的闭包)?

3 个答案:

答案 0 :(得分:5)

首先定义一个查询strings,它在查询时知道如何生成字符串序列。每次要求它时,它都会生成一个新数字并将其转换为字符串。

然后您声明一个变量outValue,并为其指定0

然后定义一个新查询someEnumerable,它知道在询问值时如何从查询strings获取下一个值,尝试解析值,如果值可以解析,产生outValue的值。我们再一次定义了一个可以执行此操作的查询,我们实际完成任何

然后,您将outValue设置为3

然后你问someEnumerable它的第一个值,你要求Select实现它的值。要计算该值,它会向Where询问的第一个值。 Where会询问strings。 (我们现在跳过几个步骤。)Where将获得0。它将在0上调用谓词,专门调用int.TryParse。这样做的副作用是outValue将设置为0TryParse会返回true,因此会产生该项目。 Select然后使用其选择器将该值(字符串0)映射到新值。选择器忽略该值并在该时间点生成outValue的值,即0。我们的foreach循环现在对0执行任何操作。

现在我们在循环的下一次迭代中询问someEnumerable第二个值。它询问Select一个值,Select询问Where,要求stringsstrings产生"1"Where调用谓词,将outValue设置为1作为副作用,Select会产生outValue的当前值,即1foreach循环现在对1执行任何操作。

所以关键在于,由于WhereSelect推迟执行的方式,只在需要值时立即执行工作,{{1}的副作用最终在Where中的每个投影之前调用谓词。如果没有推迟执行,而是在Select中的任何投影之前执行了所有TryParse次调用,那么你会看到每个值Select。我们实际上可以很容易地模拟这个。我们可以将100的结果具体化为一个集合,然后看到WhereSelect一遍又一遍地重复的结果:

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);
}