Anonymous Method(匿名メソッド)
ここには、C#2.0から追加されることとなったAnonymous Methodについて 書いてあります。なおこの記事は2005年8月現在入手可能な Visual Studio 2005 Beta2及び、C# Language Specifications 2.0 を元に書かれています。
他にメソッドを用意する必要はない
C#にはデリゲートというものがあり、これを利用してイベントなどの機能が 実現されていました。Windowsプログラムを作る場合にはよく使う機能だと思います。 これまでデリゲートを使用する時には、まずデリゲートを宣言して、それに渡す メソッドを別に定義します。あとは必要に応じてデリゲートのインスタンスを作成して メソッドを適切な場所で呼び出せばできました。ここで、時として、デリゲートに渡す ためのメソッドを別に作成するまでもないような処理が発生することがあります。 すると、メソッド名を考えて、定義して、などという手間が面倒になってきます。 できることなら、デリゲートのインスタンスを作成する段階で、同時にそれに 渡すメソッドも定義できたら便利でしょう。これを実現してくれるのが、 Anonymous Method(匿名メソッド)です。次に例を見てみましょう。
まず、従来の方法によるデリゲートの使用例をみてみます。
class Program
{
static void Main()
{
D d = new D(Msg);
d();
}
static void Msg()
{
Console.WriteLine("Hello");
}
}
次に上記のコードをAnonymous Methodを用いて書き換えてみると次のようになります。
class Program
{
static void Main()
{
D d = delegate { Console.WriteLine("Hello"); };
d();
}
}
デリゲートに渡していたMsg()というメソッドが無くなっているのがわかると思います。 代わりに、デリゲートのインスタンスを生成するときにdelegateというキーワード の後に{・・・}という部分がありますが、これがデリゲートに渡す メソッドの本体にあたります。このメソッドには名前がないことから Anonymous Methodの名前がついたのでしょう。
引数や戻り値を設定することもできる
Anonymous Methodは名前が無いだけでその他は通常のメソッドと殆ど変わりません。 例えば、戻り値を設定することもでき、引数を取ることもできます。 次にそれらの例を示します。
class Program
{
static void Main()
{
Calc add = delegate(int x, int y) { return x + y; };
Console.WriteLine(add(3, 5));
}
}
Calcは二つの整数型の引数をとり、整数型を返すデリゲートです。実際にインスタンス する行も見てもらえればわかるようにdelegateキーワードの後に引数があり、更に メソッド内部ではreturnによって値が返されています。通常のメソッドであれば、 戻り値の型が宣言されていますが、Anonymous Mehodの場合デリゲートの型等から コンパイラが推測して自動的に付加してくれるので、問題ありません。
さて、単純にこれだけなら、Anonymous Methodは我々がデリゲートに渡す 別のメソッドを用意する手間を省いてくれているにすぎません。しかし、 Anonymous Methodが本当の力を発揮するのは、メソッド本体(ブロック)の 外部にある変数にアクセスしたときです。その例をみてみましょう。
class Program
{
static void Main()
{
string msg = "Hello";
D d = delegate { Console.WriteLine(msg); };
d();
}
}
もしAnonymous Methodがメソッド本体の部分を別個のメソッドに書き出して デリゲートインスタンスを作成するだけの機能であったら、上記のような コードを実行することはできません。何故なら、msgという変数はMainメソッド 内だけのスコープしかもっていないからです。ところが、実際Anonymous Method ではその外部の変数を取り込んで参照することができます。では、次のような 2種類のコードはどのような結果になるでしょうか?
class Program
{
static void Main()
{
foreach (D d in F())
d();
}
static D[] F()
{
D[] result = new D[3];
for (int i = 0; i < 3; i++)
{
int x = i * 2;
result[i] = delegate { Console.WriteLine(x); };
}
return result;
}
}
class Program
{
static void Main()
{
foreach (D d in F())
d();
}
static D[] F()
{
D[] result = new D[3];
int x;
for (int i = 0; i < 3; i++)
{
x = i * 2;
result[i] = delegate { Console.WriteLine(x); };
}
return result;
}
}
コードの流れは、F()というメソッドで3つのデリゲートインスタンスを生成 してそれを配列として返しています。Main()ではそれらを順番に実行しているだけです。結果はそれぞれ次のようになります。
2
4
4
4
両者の違いはAnonymous Methodの中で参照している変数がfor文のスコープ 内で宣言されているか否かだけです。どうしてこのようなことが起こるのかを Anonymous Methodの実装を見ることで確認したいと思います。
Anonymous Methodの実装
Anonymous Methodのトリックは全てコンパイラにあります。つまり、 我々の書いたAnonymous Methodをコンパイラが解釈して、C#1.1でも コンパイルできるようなコードに変換しているにすぎないのです。
コンパイラによる変換ではAnonymous Methodの形態に応じて 大きく分けて2種類のコードが生成されます。それは、 Anonymous Methodが外部の変数を参照している場合は ネストしたクラスを生成し、そうでない場合は 単純にメソッド部分を切り出して別個のメソッドを定義します。 次にそれらの例をみてみます。
まず、Anonymous Methodが外部の変数を参照していない場合の例です。
class Program
{
static void Main()
{
D d = delegate { Console.WriteLine("Hello"); };
d();
}
}
これは一番最初に提示した例です。 さて、これがコンパイルされるとどうなるのでしょうか? 実際コンパイルされたコードはILなので、それを逆コンパイル したものを次に示します。(一部整理してあります。)
{
private static D <>9__CachedAnonymousMethodDelegate1;
private static void Main()
{
if (Program.<>9__CachedAnonymousMethodDelegate1 == null)
{
Program.<>9__CachedAnonymousMethodDelegate1 = new D(Program.<Main>b__0);
}
Program.<>9__CachedAnonymousMethodDelegate1();
}
private static void <Main>b__0()
{
Console.WriteLine("Hello");
}
}
一番はじめにAnonymous Methodを使用しないデリゲートの例で
提示したコードと殆ど同じ実装をしています。つまり、単純に
Anonymous Methodのメソッド本体を切り出して、別個に、ここでは
次は外部の変数を参照している場合です。
class Program
{
static void Main()
{
string msg = "Hello";
D d = delegate { Console.WriteLine(msg); };
d();
}
}
これも、一度みたサンプルです。では、これがコンパイルされると どうなるのでしょうか?
{
private sealed class <>c__DisplayClass1
{
public string msg;
public void <Main>b__0()
{
Console.WriteLine(this.msg);
}
}
private static void Main()
{
Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
class1.msg = "Hello";
D d1 = new D(class1.<Main>b__0);
d1();
}
}
新たに<>c__DisplayClass1というネストしたクラスが追加されています。 そして、そのフィールドとして元のソースではMain()のローカル変数であった msgが定義されています。つまり、外部の変数をクラス内に取り込んで メソッドが一皮被ったかたちになっています。こうすることによって、 単なるメソッドの切り出しでは実現できない外部の変数への参照を 可能にしているのです。
では、さきほどのAnonymous Method内で参照している変数の宣言位置によって 結果が変化する例はどのようになっているのでしょうか?ここでは F()がどのようにコンパイルされているのかを確認します。それぞれ 次のようになります。
{
D[] dArray1 = new D[3];
for (int num1 = 0; num1 < 3; num1++)
{
Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
class1.x = num1 * 2;
dArray1[num1] = new D(class1.<F>b__0);
}
return dArray1;
}
{
D d1 = null;
Program.<>c__DisplayClass2 class1 = new Program.<>c__DisplayClass2();
D[] dArray1 = new D[3];
for (int num1 = 0; num1 < 3; num1++)
{
class1.x = num1 * 2;
if (d1 == null)
{
d1 = new D(class1.<F>b__0);
}
dArray1[num1] = d1;
}
return dArray1;
}
つまり、ソースではどちらも3つのデリゲートインスタンスを生成しているように 書いてありますが、実際にはコンパイラが最適化して後者のほうはデリゲート インスタンスはひとつしか生成されません。従って、最後に代入された4という 値が全て表示されるというわけです。
Anonymous Methodの応用
以上のような性質を持ったAnonymous Methodですが、どのような応用例が あるのでしょうか。実はこのような性質を備えた関数はC#以外の言語にも 存在し、主に関数型言語と呼ばれる言語では昔からよく使用されています。 これらについては回を改めて書きたいと思います。