在闭包中,是什么触发了捕获变量的新实例?

时间:2013-11-21 10:39:42

标签: c# closures

我正在读Jon Skeet的C# in Depth

在页156,他有一个例子,清单5.13“捕获多个代理的多个变量实例”。

List<ThreadStart> list = new List<ThreadStart>();

for(int index=0; index < 5; index++;)
{
    int counter = index*10;
    list.Add(delegate
          {
              Console.WriteLine(counter);
              counter++;
          }
        );
}

foreach(ThreadStart t in list)
{
    t();
}

list[0]();
list[0]();
list[0]();

list[1]();

在此列表之后的解释中,他说“在这种情况下,每个委托实例都捕获了一个不同的变量。”

我理解这一点已经足够了,因为我理解每次关闭一个变量时,编译器会生成IL,将其封装在一个新的类中,专门用于捕获该变量(实质上使它成为一个引用类型,以便值它指的是不会被当前执行范围的堆栈框架破坏。)

然后他谈到我们直接捕获index而不是创建counter变量会发生什么 - “所有代表都会共享同一个变量”。

这个我不明白。 indexcounter的范围不同吗?为什么编译器也不为每个代理创建index的新实例?


注意:我想我在输入这个问题时想出来了,但我会在这里留下问题给后人。我认为答案是index实际上与counter的范围不同。索引基本上被声明为“在for循环之外”......每次都是相同的变量。

看一下为for循环生成的IL,它证明变量是在循环外声明的(lengthi是在for中声明的变量循环声明)。

.locals init (
    [0] int32 length,
    [1] int32 i,
    [2] bool CS$4$0000
)

IL_0000: nop
IL_0001: ldc.i4.s 10
IL_0003: stloc.0
IL_0004: ldc.i4.0
IL_0005: stloc.1
IL_0006: br.s IL_001b
// loop start (head: IL_001b)
    IL_0008: nop
    IL_0009: ldloca.s i
    IL_000b: call instance string [mscorlib]System.Int32::ToString()
    IL_0010: call void [mscorlib]System.Console::WriteLine(string)
    IL_0015: nop
    IL_0016: nop
    IL_0017: ldloc.1
    IL_0018: ldc.i4.1
    IL_0019: add
    IL_001a: stloc.1

    IL_001b: ldloc.1
    IL_001c: ldloc.0
    IL_001d: clt
    IL_001f: stloc.2
    IL_0020: ldloc.2
    IL_0021: brtrue.s IL_0008
// end loop

我认为本书可能在这个主题上做得更好的一件事就是解释编译器正在做什么,因为如果你理解编译器在新类中包含闭合的变量,那么所有这些“魔法”都是有意义的。

请纠正我可能存在的任何误解或误解。另外,请随时详细说明和/或添加我的解释。

2 个答案:

答案 0 :(得分:1)

听起来你已经找到了答案 - 每次循环时都没有获得新的index实例。如果您考虑允许修改循环内index值的方式 - 例如。如果你想跳过项目,在某些情况下将其设置为零,或者你喜欢的任何其他东西,你可以增加它 - 应该很清楚你只有一个index实例,而不是一个新实例每次迭代。

另一方面,每次迭代都会创建一个新的counter - 如果你在该循环的底部对它进行了更改,它对counter变量没有任何影响。下一次迭代使用。

foreach循环用于重用其循环变量,与for循环相同,这是人们常见的问题 - 请参阅Is there a reason for C#'s reuse of the variable in a foreach?

Eric Lippert explains他们已经在C#5中更改了foreach,每次都会获得一个新变量,并且他们将按原样离开for

答案 1 :(得分:0)

据我所知,闭包。它是匿名委托,它触发为委托代码块中涉及的变量创建一个类。请考虑以下代码:

class SomeClass
{
    public int counter;

    public void DoSomething()
    {
        Console.WriteLine(counter);
        counter++;
    }
}

//... 

List<ThreadStart> list = new List<ThreadStart>();

for (int index = 0; index < 5; index++)
{
    var instance  = new SomeClass { counter = index * 10 };
    list.Add(instance.DoSomething);
}

foreach (ThreadStart t in list)
{
    t();
}

此代码与原始示例中的代码完全相同。变量instance在for循环内部定义,因此其范围在每次迭代时结束,但它不会被垃圾收集器释放,因为它由list引用。这就是为什么在匿名委托的情况下创建类的原因。否则你不能这样做。