我目前正在阅读Albahari的O'Reily书, C#in a Nutshell ,并在Linq查询章节中。他在描述Linq查询时描述了延迟执行和变量捕获的影响。他给出了以下一个常见错误的例子:
IEnumerable<char> query = "Not what you might expect";
string vowels = "aeiou";
for (int i = 0; i < vowels.Length; i++)
{
query = query.Where(c => c != vowels[i]);
}
foreach (var c in query)
{
Console.WriteLine(c);
}
Console.Read();
枚举查询后会抛出IndexOutOfRangeException
,但这对我没有任何意义。由于延迟执行和变量捕获的影响,我希望Where运算符c => c!= vowles[i]
中的lambda表达式只会在c => c != vowels[4]
处对整个序列进行求值。我继续调试以查看抛出异常时发现的值i
是多少,并发现它的值为5?所以我继续将for循环中的条件子句更改为i < vowels.Length-1;
,实际上没有抛出任何异常。 for循环是否在最后一次迭代时将i
迭代为5,还是linq正在进行其他迭代?
答案 0 :(得分:4)
对于所有意图和目的(除捕获的变量之外),这个:
for (int i = 0; i < 10; i++)
....
可以改写为:
int i = 0;
while (i < 10)
{
....
i++;
}
如您所见,迭代仅在条件为假时停止,并且对于条件为假,我必须等于或大于10.
事实上,如果我在LINQPad中尝试此程序:
void Main() { }
public static void Test1()
{
for (int i = 0; i < 10; i++)
Console.WriteLine(i);
}
public static void Test2()
{
int i = 0;
while (i < 10)
{
Console.WriteLine(i);
i++;
}
}
然后检查生成的IL,让我把这两种方法放在一起:
Test1: Test2:
IL_0000: ldc.i4.0 IL_0000: ldc.i4.0
IL_0001: stloc.0 // i IL_0001: stloc.0 // i
IL_0002: br.s IL_000E IL_0002: br.s IL_000E
IL_0004: ldloc.0 // i IL_0004: ldloc.0 // i
IL_0005: call System.Console.WriteLine IL_0005: call System.Console.WriteLine
IL_000A: ldloc.0 // i IL_000A: ldloc.0 // i
IL_000B: ldc.i4.1 IL_000B: ldc.i4.1
IL_000C: add IL_000C: add
IL_000D: stloc.0 // i IL_000D: stloc.0 // i
IL_000E: ldloc.0 // i IL_000E: ldloc.0 // i
IL_000F: ldc.i4.s 0A IL_000F: ldc.i4.s 0A
IL_0011: blt.s IL_0004 IL_0011: blt.s IL_0004
IL_0013: ret IL_0013: ret
然后你可以看到它生成了完全相同的代码。
现在,编译器将确保您无法在尝试访问变量的for循环之后编写代码,但如果您捕获变量(如代码所示),那么您将访问变量就像循环结束时一样,当条件为假时,循环只会自行结束。
因此,您假设i
将等于字符串中最后一个字符的索引是false,它将等于刚刚超过它的索引,因此当您尝试时,您将获得超出范围异常的索引执行委托。
这是一个简单的.NET Fiddle,演示了这个程序:
using System;
public class Program
{
public static void Main()
{
Action a = null;
for (int index = 0; index < 10; index++)
a = () => Console.WriteLine(index);
a();
}
}
输出10。
答案 1 :(得分:1)
这是你的lambda,一个在另一个中声明的函数,可以引用父函数中的变量:
threads
c => c != vowels[i]
函数实际上不会调用lambda函数,直到您尝试迭代Where
循环中的结果序列。与使用变量的值的常规指令(例如foreach
)不同,lambda中的Console.WriteLine(i);
指的是实际变量i
。因此,一旦完成第一个循环,您创建的每个lambda都引用相同的变量i
。
当最终评估lambda时,i
是i
,这是一个超出您尝试访问的序列边界的索引。然后你的程序崩溃了。
您应该将vowels.Length
循环更改为:
for
在循环的每次迭代中重新创建for (int i = 0; i < vowels.Length; i++)
{
int index = i;
query = query.Where(c => c != vowels[index]);
}
变量,因此您创建的每个lambda引用具有不同值的不同变量。
答案 2 :(得分:0)
为了帮助您理解这一点,请尝试调试以下代码并查看输出窗口。
private void button1_Click(object sender, EventArgs e)
{
IEnumerable<char> query = "Not what you might expect";
string vowels = "aeiou";
for (int i = 0; i < vowels.Length; i++)
{
Console.WriteLine("out: " + i);
query = query.Where(c =>
{
Console.WriteLine("inner: " + i);
return c != vowels[i];
});
}
Console.WriteLine("before query");
foreach (var c in query)
{
Console.WriteLine(c);
}
Console.Read();
}