2009年1月2日金曜日

デリゲートの等価性

以前このエントリに書いた事は間違っていた。デリゲートの等価性は参照先のメソッドが等しいかどうかに従って判断される。

簡単なWindows Formsアプリケーションのプロジェクトを作って実験してみた。

Form1にbutton1を配置しそのクリックイベントにbutton1_Clickを指定して、
[1]
 private void button1_Click(object sender, EventArgs e)
 {
  EventHandler A = new EventHandler(a);
  EventHandler B = new EventHandler(a);
  MessageBox.Show((A==B).ToString());
 }
 private void a(object sender, EventArgs e)
 {
 }
と記述する。これで実験してみると、Trueと表示された。

結局、デリゲートへの参照フィールドの等価性を評価する時には、参照先のデリゲートオブジェクトのインスタンスが異なるかどうかに関わらずデリゲートオブジェクトのインスタンスが参照するメソッドが等しければ等しいと判断される。

恐らくこの理解すら間違いで、実状は以下のようになっている。そもそも、以前、win32APIの関数ポインタを引数として取れる場面でデリゲートを渡せる事を知った時随分と奇妙な感じがした。デリゲートが内部的に関数ポインタを保持している特殊なオブジェクトならばこのような事が可能である筈がない。その経験から言って以下の事は正しい。

恐らく、デリゲートオブジェクトのインスタンスなんて物は存在せず、デリゲートフィールドにはメソッドのエントリポイントへの参照(関数ポインタ)が直接格納されているのだろう。

つまり
 EventHandler A = new EventHandler(a);
と書いたからと言って、参照の関係は
 A -> EventHandlerオブジェクトのインスタンス in ヒープ
 EventHandlerオブジェクトのインスタンスが持っている隠蔽された内部のフィールド -> aメソッド
とはなっていなくて、
 A -> aメソッド
となっているわけだ。

C言語の時代にはポインタをアドレスと理解する事が、ポインタの抽象性を破壊するために学習者にとって望ましくないとされつつもポインタ自身抽象概念に徹する事ができていなかったためにそのような理解は結局必要悪であった。

同じ事がデリゲートに対しても言えて、その実状が関数ポインタであるというのは単なる内部実装であってデリゲート自体は抽象概念であるわけだが、それでも内部的実装が関数ポインタである事をC#のユーザは知っている必要があるだろう。どうやらデリゲートオブジェクトのインスタンスという物は存在しないようだから、見た目上クラスのインスタンスを作成する記法と一貫性がある
 EventHandler A=new EventHandler(a);
という記法は完璧なミスリーディングだ。C#の歴史的には後から導入された
 EventHandler A=a;
という記法の方が余程本質的であるという事になる。実際、コードを
[2]
  EventHandler A = a;
  EventHandler B = a;
  MessageBox.Show((A==B).ToString());
と書き換えても結果はTrueであった。Aにはaメソッドへの参照が、Bにもaメソッドへの参照が格納されていると理解すれば、この結果を理解する事は容易い。そして、[1]は見かけによらず[2]と等価なコードであって、デリゲートオブジェクトのインスタンスなるものは生成されはしない。

しかしながらこのような折衷様式を取る事で、Win32APIに関数ポインタと同様にデリゲートを渡す事ができ、かつ、メソッドをオブジェクトでラップしたかのように記述して(以上の考察から分かるように、実際には全くラッピングなんて行われていないのだが)オブジェクト指向言語の範疇でメソッドという概念を整合的に扱う事ができるわけだ。C#の、アカデミックな場面で議論されている最新のプログラミング言語の理論を取り入れながらもJavaと違い実用性のためにはダーティーな内部構造を選ぶ事も辞さないという潔さが良く現れている。アカデミックな側面を持ちつつお高く止まらない、うん、C#は実に良い言語だ。

なお、
[3]
 EventHandler A = delegate(object sender_, EventArgs e_) { a(sender_, e_); };
 EventHandler B = delegate(object sender_, EventArgs e_) { a(sender_, e_); };
 MessageBox.Show((A == B).ToString());
こういうコードを書くと結果はFalse。二つの行それぞれで暗黙に別々のメソッドが生成されているわけだ。匿名メソッドと呼ばれている所以である。

追記:.net ReflectorでSystem.Delegateを見てみたけれど、やっぱりデリゲートはちゃんとしたインスタンス化できるごくごく普通のオブジェクトでC#の言語仕様の中で例外的に扱われているわけではない.。System.Delegateはそれが継承しているObject.Equalsメソッド(それと ==, != 演算子)をオーバーライドしていて、
 internal object _target;
 internal IntPtr _methodPtr;
の二つのフィールドを比較するように実装されているので、参照先のメソッドが一致するかどうかでデリゲートが一致するかどうかが決まっているという訳だ(実際はもう_methodPtrAuxとか_methodBaseとかのフィールドもあってややこしい。ちょっと調べてみたが良く分からん。)。

それでは、どうしてWin32APIにデリゲートを渡せるのかというと、DllImport属性が付されたメソッドに関しては関数ポインタは自動的に__stdcall呼び出し規約が仮定され、他のマネージドな型とアンマネージドな型の間のマーシャリングと同様にデリゲートも、関数ポインタに変換される。具体的には、そのデリゲートを呼び出し、自身は__stdcall呼び出し規約で呼び出し可能な関数(Thunk)が暗黙に作成されて、そこへの関数ポインタへと変換される。決して内部的にデリゲートが関数ポインタそのものなのではなく、やっぱり暗黙の変換プロセスが仲介しているわけだ。ようやくすっきりした。引用したMSDNのページではWin32APIよりもCOMインタフェース寄りの解説になっているので、こっちのページも合わせて参照の事。

まとめると、やっぱり、見た目通りに[1]ではそれぞれの行で異なるEventHandlerのインスタンスが作成される。ただし、EventHandlerのEqualsメソッド, ==, != 演算子はいわゆる値比較の方法をとるようにオーバーライドされている。従ってイベントに+=new EventHandler(a)した後、そのイベントに-=new EventHandler(a)すると、ちゃんとイベントからaメソッド呼び出しが削除される。C#では参照型フィールドの比較は基本的に参照先のインスタンスの比較なので、色々うがった見方をして[2]の方が[1]より本質的と書いたけど、Equalsと ==, != 演算子が値比較でオーバーライドされているのならば、それ自体はよくやる普通の事だから、まあいちゃもんだった。それと、これらの考察に従えば、[1]と等価な[2]でも、暗黙に二つの異なる(しかし==の関係を満たす)EventHandlerのインスタンスが作成されるのだろう(未確認)。

0 件のコメント: