奇想曲 in C#

2010-07-17

Delegateの細かいこと

C#プログラミングにおいて、もはやdelegateの知識は不可欠のものであるが、同時に文法的に「あれ?こんなはずでは…」と思わせる所がdelegateまわりに多いのも確かである。手続き型言語に後から関数言語の概念を取り入れたのだからやむをえない。

しかしIDEの助けもあって、まるで野生動物が危険地帯を本能的に避けるかのごとく、多くのプログラマはいつしか「何となくだけど、そう書けばコンパイラが文句を言わないから」と人間=学習機械の本領を発揮してうまくdelegateと共存するようになっていくようである。もちろん、言語仕様をすべて頭に入れてからでないとプログラムなんて書けるか、という人間コンパイラかDijkstraかというような人も稀にはおいでだろうが。


ここでは、ふだん知らなくてもなんとかなっている所について、言語仕様から見直してみたいと思う。


まずは短いクイズから。

1. 匿名メソッドとラムダ式の違い

匿名メソッドとラムダ式というのは記法的な違いであってIL(中間言語)レベルでは区別されないという理解で問題はない。合わせて「匿名関数(Annonymous Function)」と呼ぶ。ではその記述能力において違うところはどこか?


答1. ラムダ式の本体にはステートメントまたは式を持たせることができる。

Func<bool> d1 = () => true;
Func<bool> d2 = delegate { true }; // Error: CS1002
Action a1 = delegate { true; }; // Error: CS0201

よく知られている基本事項である。これに対して匿名メソッドの本体にはステートメントのみが許され、しかも必ずブロック({ })になっていなければならない(「メソッド」というからには当然であろう)。2行目の"{ }"の中に式である"true"は書けないし、3行目の"true;"はC/C++ではステートメントであるが、C#ではステートメントにはなれない。メソッドブロック内にいきなり"true;"と書けないのと同じである。

また、式ツリー(Expression<TDelegate>)に変換できるのも、式を持つラムダ(式形式のラムダ)のみである。


答2. ラムダ式のパラメータリストにおいては、仮引数の型を省略できる。省略された引数の型は型推論により確定される。

これも周知の事項であると思う。


答3. 匿名メソッドではパラメータを省略できる。

この3番目の答がすぐに出たとしたら、相当言語の細部に敏感な人である。

Func<bool> d2 = delegate { return true; };
Func<bool> d2 = delegate () { return true; }; // 上の非省略形
Func<int, bool> d2 = delegate { return true; };
Func<int, bool> d2 = delegate(int i) { return true; }; // 上の非省略形

単に省略できるだけではない。上記コードの3行目のように、代入においてdelegate型の一致を確認する際にパラメータの一致を無視してもらえるのである。


2. 匿名関数は式か?

匿名関数はステートメントから成る本体を持てるが、本体がステートメントであるだけであって、匿名関数全体がステートメントであるわけではない。メソッド定義がステートメントでないのと同じことである。

では、匿名関数は式なのであろうか?


言語仕様上は確かに式である(但し、ラムダ式と匿名メソッドでは文法要素としての位置が正確に言うと異なる)。しかし、式を書ける場所ならどこでも匿名関数が書けるわけではない。次の例において式として使えないのは、ステートメントのセマンティクスが匿名関数を許していないからである。

lock (() => { }) { }  // Error
if (() => { }) { }  // Error(但しこれは構文的エラー)
throw () => { };  // Error
Delegate F() { return () => { }; }  // Error

lock, if, throwなどのステートメントが要求する式については自明であるので、それほど悩むことはないだろうが、最後のDelegate型を返すメソッドのリターン値にはなぜ使えないのであろうか?この点を次節で見てみたい。


3.匿名関数はDelegate型ではない?

今度は型の問題である。先ほどの問題をよりシンプルに考えるため、次のような代入について調べることにする。

Action a = () => { };  // OK
object o = () => { }; // CS1660 (Delegate型に変換できません)

匿名関数をオブジェクトとして扱う時には、一旦ActionやFuncのようなDelegate型に変換してから扱うというイディオムは良く知られているものの、何となく経験則的にそうしている場合が多い。しかしこれは関数型言語の感覚からするとおかしな制約である。ラムダは一級のオブジェクトではなかったのか。


鍵は、


匿名関数は式であるが、式としての型を持たない


という点である。

型を持たないのだから、objectへの代入はおろか、Delegate型への代入もできないわけである。しかし、上の例ではAction(Delegateの派生型)に代入できているではないか?

実はここに誤解を生みやすいもう一つのポイントがある。匿名関数をシグネチャ互換性のあるDelegate型に代入しようとすると、コンパイラは自動変換を行ってくれるのである。

Action a1 = () => { };
Action a2 = new Action(() => { });
Action a3 = (Action)(() => { });

この3つの文はILにおいて等価である。いずれも実際にはActionのコンストラクタが、匿名関数のシグネチャの一致をチェックした上で、匿名関数をDelegateオブジェクトに変換してくれる。これに対し、Object型への代入文には変換機能がないので(シグネチャの一致判定ができないので)、エラーとなるのである。


ところで、もし匿名関数も型を持つオブジェクトであると言語仕様を変えようと考えたらどうであろうか?

ここで、本稿の冒頭「匿名メソッドではパラメータを省略できる」を想起するべきである。つまり、C#2.0の頃から存在していたパラメータを省略された匿名メソッドは、シグネチャが不定であるため型を特定できないので仕様上の障害となる可能性が出てくる。便宜的に、パラメータを省略された場合はパラメータなしとして扱うことはできるかもしれない。しかしそうすると、例えばAction型とみなされる"delegate { ... }"は、Action<int>型と互換であるわけであるから、Action型とAction<int>型と互換でなければならなくなってしまう。もちろんこれはC#の型システムでは許されない。

要するに、匿名関数は型システムに適合させるには曖昧すぎるのである。


匿名関数はDelegate型ではない。そもそも型を持っていないのでオブジェクトですらない。

※しかし、(小文字の)delegateでない、と言ってしまうと日常会話に支障をきたすであろう。


4.Delegateの変換

次に、この変換という観点から、匿名関数でない、名前つきメソッドについてもひとつだけ注意を加えておきたい。

bool fun() { return true; }
...
Func<bool> f1 = fun;
Func<bool> f2 = new Func<bool>(fun);

もっとも普通のDelegate型オブジェクトの構築である。引数のfunはメソッド名である。前節の結果から、ここでもただの代入ではなくDelegate変換が行われていることは、もう指摘しなくても気づいていただけると思う。ではメソッド名とはいかなる存在なのか。メソッド名も匿名関数と同じと考えてよいのであろうか?

C#では、メソッド名という代わりに、「メソッドグループ」という言い方をしている。オーバーロードのあるメソッドは複数の定義に同じ名称が付いていることから、オーバーロードのある場合を含めてメソッドグループと呼ぶわけである。


匿名関数もメソッドグループも型を持たない式であるとされているが、メソッドグループが与えられた場合のDelegate変換は、メソッドグループ変換とよばれ、匿名関数の変換とは一応区別されているようである。


5.型推論とDelegate変換

Delegate変換についてよくわかった所で、次の例を見てみたい。

var v = () => {};  // Error: CS0815

型を持たない右辺式を型推論することはできないとも言えるが、右辺のシグネチャに一致するDelegateを探してきてあてはめるのは型推論の領域を超えている。型推論によって変換先となるDelegate型が決まらないのなら、Delegate変換もできない。


6.匿名関数の呼び出し

最後に次の例を見ておきたい。関数言語なら当たり前の書き方がエラーとなる。なぜだかわかるだろうか?

bool b = (() => true)();  // Error: CS0149
bool b1 = (delegate { return true; })();  // Error: CS0149
if ((() => true)())  // Error: CS0149
{ /* ... */ }


C#ではメソッド呼び出しは次の形式を取る。

<primary-expression>( [<argument-list>] );

基本式(primary-expression)は式のサブカテゴリであって、匿名メソッドもこのカテゴリに含まれる。これだけなら、少なくとも匿名メソッドに対してメソッド呼び出しを行えそうであるが、メソッド呼び出し構文に対するさらなる制約として、


メソッド呼び出し構文の基本式として使用できるのは、メソッドグループまたはDelegate型の値のどちらかに限られる


という仕様が存在する。そのため、匿名関数をここで用いることはできないようになっている。

推測であるが、これを許しても構文が複雑になるだけで実質的に意味のあるコードが書けないことを勘案したのではないだろうか。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/toshi_m/20100717/1279368502
Connection: close