为什么我的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(访问修改的闭包)?

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

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

然后声明一个变量outValue,并将0分配给它

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

然后将outValue设置为3

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

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

因此,这里的关键点是,由于WhereSelect延迟执行的方式,只有在需要值时才立即执行它们的工作,因此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);
}