雑記 - otherwise

最近はDQ10しかやっていないダメ技術者がちまちまと綴る雑記帳

続続・ラムダ式の変数スコープ

前の記事でかるあさんに指摘されてコメントした件について、改めて MSIL をみてみました。
まずは検証に使ったコードを書いておきます。(簡単なコードだったので、デザイナ部分を含めて全部直書きしました)

using System;
using System.Drawing;
using System.Windows.Forms;

class Foo : Form {
  private Button button1;
  private Button button2;

  public Foo() {
    InitializeComponent();
    var i = 0;
    button1.Click += (sender, e) => MessageBox.Show((++i).ToString());
    button2.Click += (sender, e) => MessageBox.Show((++i).ToString());
  }

  private void InitializeComponent() {
    button1 = new Button() { Location = new Point(5, 5), Size = new Size(80, 21), Text = "一個目" };
    button2 = new Button() { Location = new Point(5, 30), Size = new Size(80, 21), Text = "二個目" };
    ClientSize = new Size(120, 65);
    Controls.Add(button1);
    Controls.Add(button2);
  }

  static void Main() {
    Application.Run(new Foo());
  }
}

これを .NET Reflector にかけて Foo クラスのコンストラクタ部分の MSIL を取り出します。

.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
    .maxstack 4
    .locals init (
        [0] class Foo/<>c__DisplayClass2 class2)
    L_0000: ldarg.0 
    L_0001: call instance void [System.Windows.Forms]System.Windows.Forms.Form::.ctor()
    L_0006: nop 
    L_0007: newobj instance void Foo/<>c__DisplayClass2::.ctor()
    L_000c: stloc.0 
    L_000d: nop 
    L_000e: ldarg.0 
    L_000f: call instance void Foo::InitializeComponent()
    L_0014: nop 
    L_0015: ldloc.0 
    L_0016: ldc.i4.0 
    L_0017: stfld int32 Foo/<>c__DisplayClass2::i
    L_001c: ldarg.0 
    L_001d: ldfld class [System.Windows.Forms]System.Windows.Forms.Button Foo::button1
    L_0022: ldloc.0 
    L_0023: ldftn instance void Foo/<>c__DisplayClass2::<.ctor>b__0(object, class [mscorlib]System.EventArgs)
    L_0029: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
    L_002e: callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::add_Click(class [mscorlib]System.EventHandler)
    L_0033: nop 
    L_0034: ldarg.0 
    L_0035: ldfld class [System.Windows.Forms]System.Windows.Forms.Button Foo::button2
    L_003a: ldloc.0 
    L_003b: ldftn instance void Foo/<>c__DisplayClass2::<.ctor>b__1(object, class [mscorlib]System.EventArgs)
    L_0041: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
    L_0046: callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::add_Click(class [mscorlib]System.EventHandler)
    L_004b: nop 
    L_004c: nop 
    L_004d: ret 
}

内部的に生成されるクラスは c__DisplayClass2 のひとつだけで、そのクラスに二つのラムダ式が作られているのが判ります。
ついでに c__DisplayClass2 の方はこんな感じです。

.class auto ansi sealed nested private beforefieldinit <>c__DisplayClass2
    extends [mscorlib]System.Object
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
        .maxstack 8
        L_0000: ldarg.0 
        L_0001: call instance void [mscorlib]System.Object::.ctor()
        L_0006: ret 
    }

    .method public hidebysig instance void <.ctor>b__0(object sender, class [mscorlib]System.EventArgs e) cil managed
    {
        .maxstack 3
        .locals init (
            [0] int32 num)
        L_0000: ldarg.0 
        L_0001: dup 
        L_0002: ldfld int32 Foo/<>c__DisplayClass2::i
        L_0007: ldc.i4.1 
        L_0008: add 
        L_0009: dup 
        L_000a: stloc.0 
        L_000b: stfld int32 Foo/<>c__DisplayClass2::i
        L_0010: ldloc.0 
        L_0011: stloc.0 
        L_0012: ldloca.s num
        L_0014: call instance string [mscorlib]System.Int32::ToString()
        L_0019: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
        L_001e: pop 
        L_001f: ret 
    }

    .method public hidebysig instance void <.ctor>b__1(object sender, class [mscorlib]System.EventArgs e) cil managed
    {
        .maxstack 3
        .locals init (
            [0] int32 num)
        L_0000: ldarg.0 
        L_0001: dup 
        L_0002: ldfld int32 Foo/<>c__DisplayClass2::i
        L_0007: ldc.i4.1 
        L_0008: add 
        L_0009: dup 
        L_000a: stloc.0 
        L_000b: stfld int32 Foo/<>c__DisplayClass2::i
        L_0010: ldloc.0 
        L_0011: stloc.0 
        L_0012: ldloca.s num
        L_0014: call instance string [mscorlib]System.Int32::ToString()
        L_0019: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
        L_001e: pop 
        L_001f: ret 
    }


    .field public int32 i

}

b__0() と b__1() でメンバ変数 i を共有しているのが判ります。

内部クラスの個数

先の記事にコメントした際、「同じスコープ内で定義されたラムダ式は、ひとつの内部クラスにまとめられる」と書いたのですが、ちょっと違いました。
例えば、↓こんな事をやると内部クラスは 2 つ生成されます。

Func<int> f;
var i = 4;
{
  var j = 5;
  f = () => i + j;
  j += 10;
}
i += 3;
Console.WriteLine(f());

ラムダ式に取り込む変数 (i, j) のスコープをずらしてみました。
この場合の MSIL は↓こんな感じになります。

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 3
    .locals init (
        [0] class [System.Core]System.Func`1<int32> func,
        [1] class Foo/<>c__DisplayClass3 class2,
        [2] class Foo/<>c__DisplayClass1 class3)
    L_0000: newobj instance void Foo/<>c__DisplayClass1::.ctor()
    L_0005: stloc.2 
    L_0006: nop 
    L_0007: ldloc.2 
    L_0008: ldc.i4.4 
    L_0009: stfld int32 Foo/<>c__DisplayClass1::i
    L_000e: newobj instance void Foo/<>c__DisplayClass3::.ctor()
    L_0013: stloc.1 
    L_0014: ldloc.1 
    L_0015: ldloc.2 
    L_0016: stfld class Foo/<>c__DisplayClass1 Foo/<>c__DisplayClass3::CS$<>8__locals2
    L_001b: nop 
    L_001c: ldloc.1 
    L_001d: ldc.i4.5 
    L_001e: stfld int32 Foo/<>c__DisplayClass3::j
    L_0023: ldloc.1 
    L_0024: ldftn instance int32 Foo/<>c__DisplayClass3::<Main>b__0()
    L_002a: newobj instance void [System.Core]System.Func`1<int32>::.ctor(object, native int)
    L_002f: stloc.0 
    L_0030: ldloc.1 
    L_0031: dup 
    L_0032: ldfld int32 Foo/<>c__DisplayClass3::j
    L_0037: ldc.i4.s 10
    L_0039: add 
    L_003a: stfld int32 Foo/<>c__DisplayClass3::j
    L_003f: nop 
    L_0040: ldloc.2 
    L_0041: dup 
    L_0042: ldfld int32 Foo/<>c__DisplayClass1::i
    L_0047: ldc.i4.3 
    L_0048: add 
    L_0049: stfld int32 Foo/<>c__DisplayClass1::i
    L_004e: ldloc.0 
    L_004f: callvirt instance !0 [System.Core]System.Func`1<int32>::Invoke()
    L_0054: call void [mscorlib]System.Console::WriteLine(int32)
    L_0059: nop 
    L_005a: nop 
    L_005b: ret 
}

i と j が別々の内部クラス( c__DisplayClass3 と c__DisplayClass1 )で管理されているのが判ります。
つまり、外側スコープを取り込んだラムダ式は、「取り込む変数のスコープごとに内部クラスにまとめられる」様です。
ちなみに、それぞれのクラスの MSIL も挙げておきます。

c__DisplayClass3
.class auto ansi sealed nested private beforefieldinit <>c__DisplayClass3
    extends [mscorlib]System.Object
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
        .maxstack 8
        L_0000: ldarg.0 
        L_0001: call instance void [mscorlib]System.Object::.ctor()
        L_0006: ret 
    }

    .method public hidebysig instance int32 <Main>b__0() cil managed
    {
        .maxstack 2
        .locals init (
            [0] int32 num)
        L_0000: ldarg.0 
        L_0001: ldfld class Foo/<>c__DisplayClass1 Foo/<>c__DisplayClass3::CS$<>8__locals2
        L_0006: ldfld int32 Foo/<>c__DisplayClass1::i
        L_000b: ldarg.0 
        L_000c: ldfld int32 Foo/<>c__DisplayClass3::j
        L_0011: add 
        L_0012: stloc.0 
        L_0013: br.s L_0015
        L_0015: ldloc.0 
        L_0016: ret 
    }


    .field public class Foo/<>c__DisplayClass1 CS$<>8__locals2

    .field public int32 j

}
c__DisplayClass1
.class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1
    extends [mscorlib]System.Object
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
        .maxstack 8
        L_0000: ldarg.0 
        L_0001: call instance void [mscorlib]System.Object::.ctor()
        L_0006: ret 
    }


    .field public int32 i

}

i の方は、変数管理しかしていないですね。 ^^;