長いのでサマリ:
- ラムダ式を使うと、基本的に入れ子クラスが作られ、ラムダ式が使う変数をとっておかれる。
- ラムダ式の中身に書いたものは、基本的に入れ子クラスのメソッドとして定義される。
- 入れ子クラスは不要なときには作られない。
評価スタックでピンとこない方は「C#でHelloWorldプログラムを作成する」を読んでおくのをおすすめする。
今回の説明用のサンプルコードはこちら。「.Count(x => x == val)」がどうなっていくのか、というお話。
namespace ConsoleApplicationLinq { class Program { static void Main(string[] args) { int val = 100; var format = "Count of {0}: {1}"; var array = new int[0]; var count = array.Count(x => x == val); System.Console.WriteLine(format, val, count); } } }
サイズ0のint配列に100がいくつあるか調べてWriteするというコード(0に決まっている)。説明の都合、順番などが不自然な感じになっている。
このソースをILに逆アセンブルした結果がこちら。
https://gist.github.com/sunnyone/0d3ed8285b5d91495399
別ウィンドウで開きながら説明を見るといいかも。
さて、これからILを見ていく。書いた部分がコンパイルされているであろうMainメソッドの実装を見ようとすると、そのMainメソッドの前に見知らぬ「<>c__DisplayClass1」という入れ子クラスが作られていることがわかる。
そのクラスの内容はこうなっている。
.class auto ansi sealed nested private beforefieldinit '<>c__DisplayClass1' extends [mscorlib]System.Object { (CompilerGeneratedAttributeの部分は省略) .field public int32 val // valフィールド (ここにコンストラクタの定義があるが省略) // <Main>b__0メソッドの定義 .method public hidebysig instance bool '<Main>b__0'(int32 x) cil managed { .maxstack 8 // xをロード(評価スタックにpush) IL_0000: ldarg.1 // valフィールドをロード IL_0001: ldarg.0 IL_0002: ldfld int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val // 二つの値=xとvalの値を比較し、同じなら1・異なるときは0 IL_0007: ceq // return IL_0009: ret } // end of method '<>c__DisplayClass1'::'<Main>b__0' } // end of class '<>c__DisplayClass1'
このクラスは要は「x => x == val」の部分を<Main>b__0というメソッドに実装し、加えてvalフィールドを持っている。
次に本体のMainメソッド。Mainメソッドはこの入れ子クラスを活用して動作する。
まずメソッドとローカル変数の定義。format, array, countのほかに、先ほどの<>c__DisplayClass1が「CS$<>8__locals2」として用意されているのがわかる。valはないことに注意。
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 3 .locals init ([0] string format, [1] int32[] 'array', [2] int32 count, [3] class ConsoleApplicationLinq.Program/'<>c__DisplayClass1' 'CS$<>8__locals2')
ここから処理開始だが、記述したC#コードに対応する部分に先立って、<>c__DisplayClass1がnewされ、ローカル変数に入る。
// <>c__DisplayClass1クラスのインスタンスを生成してローカル変数3番目にセット IL_0000: newobj instance void ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::.ctor() IL_0005: stloc.3
次に「int val = 100;」の部分。ここがポイント。
C#コード上ではローカル変数に見えているが、実際には内部の<>c__DisplayClass1クラスのフィールドになっている。
入れ子になったクラスにあるラムダ式の実体がこの変数を使うためにフィールドに入れている。
// CS$<>8__locals2と「100」をロードして、CS$<>8__locals2のvalにセット IL_0006: ldloc.3 IL_0007: ldc.i4.s 100 IL_0009: stfld int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val
次に「var format = "Count of {0}: {1}";」の部分。このように、ラムダ式と関係ない部分はふつうのローカル変数になる。
// 説明省略 IL_000e: ldstr "Count of {0}: {1}" IL_0013: stloc.0
次に「var array = new int[0];」だが、ここもローカル変数に入れるだけ。
// 説明省略 IL_0014: ldc.i4.0 IL_0015: newarr [mscorlib]System.Int32 IL_001a: stloc.1
次に実際のラムダ式が登場する「var count = array.Count(x => x == val);」の部分。
先ほどvalのために生成したDisplayClassのメソッドを使ってFuncオブジェクトを生成し、Countメソッドに渡している。
// ローカル変数1番目:arrayをロード IL_001b: ldloc.1 // <Main>b__0メソッドのポインタをロード IL_001c: ldloc.3 IL_001d: ldftn instance bool ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::'<Main>b__0'(int32) // <Main>b__0メソッドのポインタを使って、Funcオブジェクトを生成 IL_0023: newobj instance void class [mscorlib]System.Func`2::.ctor(object, native int) // System.Linq.Enumerable::CountメソッドにarrayとFuncオブジェクトを渡す IL_0028: call int32 [System.Core]System.Linq.Enumerable::Count (class [mscorlib]System.Collections.Generic.IEnumerable`1, class [mscorlib]System.Func`2) // ローカル変数2番目にセット IL_002d: stloc.2
最後に「System.Console.WriteLine(format, val, count);」の部分。
ポイントはvalを使うのにラムダ式用の<>c__DisplayClass1を使っているところ。
// ローカル変数0番目: formatをロード IL_002e: ldloc.0 // ローカル変数3番目: CS$<>8__locals2のvalフィールドをロード、intなのでboxingする IL_002f: ldloc.3 IL_0030: ldfld int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val IL_0035: box [mscorlib]System.Int32 // ローカル変数2番目: countをロード, boxing IL_003a: ldloc.2 IL_003b: box [mscorlib]System.Int32 // WriteLine IL_0040: call void [mscorlib]System.Console::WriteLine(string, object, object) IL_0045: ret } // end of method Program::Main
このように、ラムダ式は、必要な変数を入れ子クラスのインスタンスにとっておいて、ラムダ式に書いた内容のメソッドが実行される、という形で実装されている。
しかし、ラムダ式があれば必ず入れ子クラスが作られるかというとそうではなく、変数をとっておく必要がない場合、違う形にコンパイルされる。
たとえば、先のコードのint val = 100;にconstをつけてconst int val = 100;にするだけで、ILはこうなってしまう。
.class private auto ansi beforefieldinit ConsoleApplicationLinq.Program extends [mscorlib]System.Object { // ProgramクラスそのものにFuncのフィールドが用意される .field private static class [mscorlib]System.Func`2<int32,bool> 'CS$<>9__CachedAnonymousMethodDelegate1' (CompilerGeneratedAttributeの部分は省略) .method private hidebysig static void Main(string[] args) cil managed { // ローカル変数の定義などなど .entrypoint .maxstack 3 .locals init ([0] string format, [1] int32[] 'array', [2] int32 count) IL_0000: ldstr "Count of {0}: {1}" IL_0005: stloc.0 IL_0006: ldc.i4.0 IL_0007: newarr [mscorlib]System.Int32 IL_000c: stloc.1 IL_000d: ldloc.1 // CS$<>9__CachedAnonymousMethodDelegate1をロードして、存在すればこの先の処理まで飛ばす IL_000e: ldsfld class [mscorlib]System.Func`2ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1' IL_0013: brtrue.s IL_0026 // なければProgramクラスに定義された<Main>b__0メソッド(「x => x == val」の実装)を使ってFuncオブジェクトを作る IL_0015: ldnull IL_0016: ldftn bool ConsoleApplicationLinq.Program::'<Main>b__0'(int32) IL_001c: newobj instance void class [mscorlib]System.Func`2 ::.ctor(object, native int) // 作ったらフィールドに格納して、ロードしておく IL_0021: stsfld class [mscorlib]System.Func`2 ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1' IL_0026: ldsfld class [mscorlib]System.Func`2 ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1' // 以下同じ IL_002b: call int32 [System.Core]System.Linq.Enumerable::Count (class [mscorlib]System.Collections.Generic.IEnumerable`1, class [mscorlib]System.Func`2) IL_0030: stloc.2 IL_0031: ldloc.0 IL_0032: ldc.i4.s 100 IL_0034: box [mscorlib]System.Int32 IL_0039: ldloc.2 IL_003a: box [mscorlib]System.Int32 IL_003f: call void [mscorlib]System.Console::WriteLine(string, object, object) IL_0044: ret } // end of method Program::Main (コンストラクタの定義は省略) .method private hidebysig static bool '<Main>b__0'(int32 x) cil managed { (CompilerGeneratedAttributeの部分は省略) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldc.i4.s 100 // 「100」はここに埋め込まれるので、フィールドから取る必要がない IL_0003: ceq IL_0005: ret } // end of method Program::'<Main>b__0' } // end of class ConsoleApplicationLinq.Program
こっちのコードだとご丁寧にもFuncオブジェクトをstaticフィールドにキャッシュしている。
ちなみに、valをローカル変数ではなくフィールドに持つようなクラスを作った場合も上述のconstの形に近くなり、入れ子クラスは作られない(Funcオブジェクトのキャッシュはしなくなる)。
まぁ、この差異はプログラム全体からしたらたいしたことはないと思うので、書くときに意識することはないと思うが、知っておいても悪くない…かな?
(一応、array.Count(x => x == val)を10000000回実行したら、valがconst/フィールド/ローカル変数それぞれの場合でStopwatchクラス読みで240ms/240ms/300msだった)
こうしてILコードを見ていると、ラムダ式がどういうものなのかしっくりきたのだけど、みなさまはどうだろうか?