何时迭代变量for for循环递增

时间:2016-07-03 19:50:59

标签: c# linq

我目前正在阅读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正在进行其他迭代?

3 个答案:

答案 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时,ii,这是一个超出您尝试访问的序列边界的索引。然后你的程序崩溃了。

您应该将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();
    }