Closure 和 Linq [复制]

Closure and Linq [duplicate]

提问人:Ziv 提问时间:12/6/2021 更新时间:12/6/2021 访问量:652

问:

IEnumerable<char> charQuery1 = "Not what you might expect";
string vowels = "aeiou";
for (int i = 0; i < vowels.Length; i++)
   charQuery1 = charQuery1.Where(c => c != vowels[i]);
foreach (char c in charQuery1) Console.Write(c);

这将引发 IndexOutofRange 异常。编译器将 for 循环变量“i”的作用域定为外部声明的变量。有人可以剖析一下,让我更舒服地理解吗?调试在这里对我没有多大帮助。我在这里试图理解编译器的角色。

C# LINQ 闭包

评论

1赞 Tim Schmelter 12/6/2021
没有回答您的问题,但这将是正确的方式:charQuery1 = charQuery1.Except(vowels)
0赞 Charlieface 12/6/2021
这回答了你的问题吗?对不起,链接错误 stackoverflow.com/questions/271440/...
0赞 Charlieface 12/6/2021
您需要在循环中复制循环变量
1赞 Tim Schmelter 12/6/2021
这个答案有助于理解这个问题: stackoverflow.com/a/8993720/284240 循环变量将被转移到类中的字段。它的值在循环中增加,直到 5,foreach 将执行最终值为 5 的查询。i

答:

0赞 zhe 12/6/2021 #1

当您在代码的最后一部分枚举 charQuery1 时,您传入的谓词(即 )将以延迟方式执行 ()。到那一刻,变量的值是循环中的最后一个值,即“5”。因此,您捕获了从内部闭包修改的值。 您必须记住,LINQ 表达式具有延迟执行,并且在声明它们时执行,而是在枚举它们时执行。Wherec != vowels[i]foreach (char c in charQuery1)ifor (int i = 0; i < vowels.Length; i++)

为了避免这个问题,你应该像这样将循环索引捕获到循环作用域内的临时变量中。

IEnumerable<char> charQuery1 = "Not what you might expect";
string vowels = "aeiou";
for (int i = 0; i < vowels.Length; i++)
{
   int index = i; // Temporary index variable. Should be declared inside the scope!
   charQuery1 = charQuery1.Where(c => c != vowels[index]);
}
foreach (char c in charQuery1) Console.Write(c);

UPD:完全延迟行为:

IEnumerable<char> charQuery1 = "Not what you might expect";
string vowels = "aeiou";
Enumerable
    .Range(0, vowels.Length)
    .ForEach(i =>
    {
        charQuery1 = charQuery1.Where(c => c != vowels[i]);
    });
foreach (char c in charQuery1) Console.Write(c);

但进一步简化会给你这个:

IEnumerable<char> charQuery1 = "Not what you might expect";
string vowels = "aeiou";

charQuery1 = Enumerable
    .Range(0, vowels.Length)
    .Aggregate(charQuery1, (cq, i) => cq.Where(c => c != vowels[i]));

foreach (char c in charQuery1) Console.Write(c);

很酷,不是吗?

评论

0赞 Ziv 12/6/2021
您能更详细地解释一下延迟执行吗?当我枚举和打印时,它不会再次进入 for 循环吗?如果 i=5,则它永远不会执行。右?
0赞 zhe 12/6/2021
@Jeevan 循环立即执行,但 LINQ 表达式仅在枚举时执行。在您的示例中,您混合了这两种行为,这就是问题的原因。Id 建议坚持完全延迟的行为,并将第一个循环替换为:forEnumerable .Range(0, vowels.Length) .ForEach(i => { charQuery1 = charQuery1.Where(c => c != vowels[i]); });
1赞 Ziv 12/6/2021
谢谢!这就是我感到困惑的地方。立即执行 for 循环和延迟执行委托。现在我很清楚了。泰
0赞 Denis Sivtsov 2/7/2023
它会起作用,但不像更简单的解决方案那样优雅 ' foreach (var itemChar in vowels) charQuery1 = charQuery1.Where(c => c != itemChar);'。“完全延迟行为”一词很奇怪,因为“完全”或“不完全”不会改变任何事情
2赞 MonsterInTheBin 12/6/2021 #2

Linq 函数,例如创建将延迟计算的 IEnumerable。Where

这意味着在调用函数时不会生成函数的结果。在迭代返回的 IEnumerable by 时,将生成函数的结果。WhereWhereWhereWhere

在示例代码中,函数的结果在代码末尾的 foreach 循环中循环迭代:Where

foreach (char c in charQuery1) Console.Write(c);

这是函数返回的枚举被迭代的地方,导致执行提供给函数的谓词委托。WhereWhere

在您的例子中,这些谓词委托是 形式的匿名函数,它们在变量 上闭合。c => c != vowels[i]i

同样,仅当执行代码末尾的 foreach 循环时,才会调用这些匿名函数。c => c != vowels[i]

那么,执行 foreach 循环时的价值是什么?这是循环退出时的最后一个值。换言之,的值等于 。iforivowels.Length

这导致谓词委托等价于 。显然,将导致 IndexOutOfRangeException。c => c != vowels[vowels.Length]vowels[vowels.Length]

如何解决?

幼稚的方法是不要让谓词在 for 循环的计数器变量上闭合。相反,让它关闭一个变量,该变量的作用域 for 正文:i

for (int i = 0; i < vowels.Length; i++)
{
    var local_i = i;
    charQuery1 = charQuery1.Where(c => c != vowels[local_i]);
}

但正如 Tim Schmelter 所指出的,更优雅、更简短的解决方案是简单地使用 Linq 的方法:Except

var charQueryWithoutVowels = charQuery1.Except(vowels);

评论

0赞 Denis Sivtsov 2/7/2023
非常详细和清晰的解释,但它提供了从其他角度查看关闭 Lambda 的可能性,谢谢它帮助我。但是另一个优雅的解决方案是使用 foreach vs for ' foreach (var itemChar in vowels) charQuery1 = charQuery1.Where(c => c != itemChar);'