読者です 読者をやめる 読者になる 読者になる

雑記 - otherwise

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

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

C#

実際のところ、値型の場合の実装が( MSIL 上で)どうなっているのか興味が沸いたので調べてみました。
基にしたソースコードは以下。(伊藤さんの記事から拝借しました。 m(_ _)m )

class Foo {
  static void Main() {
    Action a;
    {
      var i = 10;
      a = () => Console.WriteLine(++i);
    }
    a();
  }
}

このコードを .NET Reflector にかけたところ、メインメソッドの MSIL はこんな感じになりました。

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 3
    .locals init (
        [0] class [System.Core]System.Action action,
        [1] class Foo/<>c__DisplayClass1 class2)
    L_0000: nop 
    L_0001: newobj instance void Foo/<>c__DisplayClass1::.ctor()
    L_0006: stloc.1 
    L_0007: nop 
    L_0008: ldloc.1 
    L_0009: ldc.i4.s 10
    L_000b: stfld int32 Foo/<>c__DisplayClass1::i
    L_0010: ldloc.1 
    L_0011: ldftn instance void Foo/<>c__DisplayClass1::<Main>b__0()
    L_0017: newobj instance void [System.Core]System.Action::.ctor(object, native int)
    L_001c: stloc.0 
    L_001d: nop 
    L_001e: ldloc.0 
    L_001f: callvirt instance void [System.Core]System.Action::Invoke()
    L_0024: nop 
    L_0025: ret 
}

どうやらラムダ式は内部的なクラスとして管理されている様です。
で、ラムダ式内で外側スコープの変数を利用すると、その内部クラスのメンバ変数として再定義されます。
そして、変数のスコープを抜ける直前で、値を内部クラスのメンバ変数にコピーする、と。(使用した変数が参照型だと参照のコピーになるのかな?)
なるほど、これでスコープを抜けても値が保持されると云うわけですね。
ちなみに、 Action デリゲートへの登録は、この内部クラスのメンバメソッドの関数ポインタを引き渡す事で実現していますね。
……ところで、この内部クラスのインスタンスのスコープってどうなっているんだろうか。(↑の例ではちょっと判らなかった。。。)

おまけ

内部クラスの中身は↓こんな感じになっていました。

.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 
    }

    .method public hidebysig instance void <Main>b__0() cil managed
    {
        .maxstack 3
        .locals init (
            [0] int32 num)
        L_0000: ldarg.0 
        L_0001: dup 
        L_0002: ldfld int32 Foo/<>c__DisplayClass1::i
        L_0007: ldc.i4.1 
        L_0008: add 
        L_0009: dup 
        L_000a: stloc.0 
        L_000b: stfld int32 Foo/<>c__DisplayClass1::i
        L_0010: ldloc.0 
        L_0011: call void [mscorlib]System.Console::WriteLine(int32)
        L_0016: nop 
        L_0017: ret 
    }

    .field public int32 i

}

こちらは、シンプルにラムダ式の実体 (b__0()) と値を保持するためのメンバ変数 (int32 i) があるだけですね。