C# 言語で C 言語の関数ポインタのように、呼び出すべきメソッドを変数のように扱ってコールバックを実現するにはデリゲートを利用しました。 デリゲートは安全にメソッドの位置を参照することができ、イベントと組み合わせることでコールバックメソッドのリストを管理することができる便利な機能となりました。
しかし、コールバックは逐次実行ではなくなるためコードとしては流れがつかみにくくなるという問題もありました。 例えば、メソッドがコールバック専用なのかどうかを判断することはできません。 また、ボタンが押された結果としてメッセージボックスひとつを表示するだけの簡単なコールバックメソッドですら、専用のメソッドを新しく追加しなければならないという面倒もあります。
delegate void CallbackMethod(); class Test { static void Main() { CallbackMethod callback = new CallbackMethod( HasNameMethod ); System.Console.WriteLine("Delegate Object = " + callback); callback(); } static void HasNameMethod() { System.Console.WriteLine("HasNameMethod"); } }
例えば、このプログラムは従来の C# 言語で作成するデリゲートです。 GUI コントロールのイベントハンドラなどを作成するには、このように専用のメソッドを作成してコールバックしてもらう必要がありました。 ボタンが押された結果としてメッセージボックス一つを表示させるためにも、メソッドを用意する必要があったのです。
C# 2.0 では Anonymous Method が追加されたことで、この問題の大部分を解決することができます。 Anonymous Method とは、デリゲートのインスタンス生成時に同時に定義される名前のないメソッドのことで、このメソッドを呼び出すにはデリゲートから呼び出さなければならないため、コールバック専用のメソッドやメソッドとして独立させるまでもない単純処理を簡単にデリゲートのインスタンスにすることができます。
Anonymous Method を用いたデリゲートインスタンスの生成には次のように記述します。
delegate { メソッド本体 };
これによって、デリゲートインスタンス以外から呼び出される可能性の無い単純な処理であれば、デリゲートインスタンス生成式の中にメソッド本体を記述することができるのです。
delegate void CallbackMethod(); class Test { static void Main() { CallbackMethod callback = delegate { System.Console.WriteLine("Anonymous Method"); }; System.Console.WriteLine("Delegate Object = " + callback); callback(); } }
>test Delegate Object = CallbackMethod Anonymous Method
このプログラムは、Anonymous Method の機能を用いて CallbackMethod 型のデリゲートインスタンスを匿名で作成しています。 専用のメソッドを用意するのではなく、ブロックの中に名前の無いデリゲート用のメソッドが記述されています。
パラメータを受け取るデリゲートに対しては、Anonymous Method もパラメータを受け取るように宣言しなければなりません。 パラメータ付の Anonymous Method は次のように記述します。
delegate (パラメータリスト) { メソッド本体 };
実は Anonymous Method の特徴として、パラメータ定義の省略があります。 先ほどのプログラムの記述方法は、メソッドのパラメータ定義を省略していただけで delegate () { ... } と記述することもできます。
Anonymous Method がパラメータを受け取っても、その値を利用しないという場合は仮パラメータリストの定義を省略しても問題はありません。
delegate void CallbackMethod(string param); class Test { static void Main() { CallbackMethod callback = delegate(string param) { System.Console.WriteLine(param); }; callback("Kitty on your lap"); //仮パラメータ定義は省略可能 callback = delegate { System.Console.WriteLine("Anonymous Method"); }; callback("Neko Mimi Mode"); } }
>test Kitty on your lap Anonymous Method
このプログラムでは string 型のオブジェクトを受け取る Callback デリゲートが宣言されています。 このデリゲートのインスタンスを Anonymous Method で作成するには、デリゲートの宣言と同じ仮パラメータリストを定義するか、定義を省略するかのいずれかです。
Anonymous Method は何らかのブロックの内部に記述されるという性質があります。 ブロックがインスタンスメソッドであれば、インスタンスフィールドにアクセスすることができますし、クラスメソッドでもクラスフィールドにならばアクセスすることができます。
しかし、一般的に外部からコールバックされる可能性のある内部のネストされたブロックが、外のブロックのローカル変数にアクセスすることはできません。 なぜならば、ローカル変数はブロックから抜け出すときにデストラクタによって削除されてしまうためです。 Anonymous Method がコールバックされるタイミングでは、ローカル変数が破棄されている可能性があるということになります。
ところが、C# 2.0 では、Anonymous Method から上のブロックのローカル変数へのアクセスを許可しています。 これは、Anonymous Method のコードが外部のローカル変数のコピーを内部で暗黙的に保存しておくことで実現しています。
delegate void CallbackMethod(); class Test { static CallbackMethod GetMethod(string param) { int i = 10; return delegate { System.Console.WriteLine("param = " + param); System.Console.WriteLine("i = " + i); }; } static void Main() { CallbackMethod callback = GetMethod("Kitty on your lap"); callback(); } }
>test param = Kitty on your lap i = 10
このプログラムの GetMethod() メソッドで定義されている Anomumous Method では、内部のブロックから外部の GetMethod() メソッドのブロックのローカル変数である param パラメータと i 整数型変数を利用しています。 これらのローカル変数は本来であればブロックを抜け出した時点でメモリから破棄されてしまいます。 制御が Main() 関数に戻った時点で、その存在は本来であれば保障されないと考えるべきです。
しかし、C# 2.0 の実装ではローカル変数を静的な変数として、Anonymous Method で暗黙的に管理されます。 つまり、記述上ではローカル変数にアクセスしているように思われますが、実際にはデリゲートインスタンスが内部で単一の変数として管理されています。 重要なのは、この変数がデリゲートインスタンス毎に用意されているわけではなく、static 修飾子のクラスフィールドのように単一で存在しているということです。
delegate void CallbackMethod(); class Test { static CallbackMethod [] GetMethods(string param) { CallbackMethod [] result = new CallbackMethod[5]; for (int i = 0 ; i < result.Length ; i++) { result[i] = delegate { System.Console.WriteLine(i); } ; } return result; } static void Main() { CallbackMethod [] callbacks = GetMethod("Kitty on your lap"); for(int i = 0 ; i < callbacks.Length ; i++) callbacks[i](); } }
>test 5 5 5 5 5
このプログラムの結果から、デリゲートインスタンスがローカル変数を参照する場合、ローカル変数の最終的な状態を保存している暗黙的な静的フィールドのようなものが存在していると考えることができます。 GetMethods() メソッドが返す複数のデリゲートインスタンスが参照している i 変数は常に一つなのです。