C#のデリゲートとイベントの使い分け

(2007.11.9)はてなブックマーク数
一部加筆(2007.11.21)
一部表現の調整(2007.12.14)
イベントハンドラについての記述の訂正等(2008.2.28)
VB6.0のイベントへの言及加筆(2008.3.4)
「デリゲート イベント」だとGoogleで上位に表示されるのに「delegate event」だとさっぱりなので、これらのキーワードを加えてみることに(2009.6.27)
追記(2010.3.5)

 C#でプログラム開発を始めて疑問に思うことの一つに、デリゲート(delegate)とイベント(event)は何が違うのかということがある。いずれも、あらかじめ定義されたデリゲート型の変数を宣言するような形で宣言し、その変数にメソッドを登録したりそれを呼び出したりして使う。それらを行う記法にもほとんど違いがない。
 唯一大きく違うように思えるのは、イベントの場合呼び出されるハンドラのシグニチャが大略

void EventHandler(Object sender, EventArgs e)

という形になるということくらいである。とはいえ、この点も本質的な差ではない。イベントの引数をこの形にするのは単なる慣習であって、異なる形にしてもエラーにはならないし、この慣習に従うとしても、EventArgsを継承して拡張すれば、デリゲート同様に付加的なデータをハンドラに渡すことはできるのだから。
 Visual Studioのデザイナでフォーム上に配置したボタンなどのイベントハンドラを作るような場合は、デザイナが勝手にハンドラをイベントとして実装するコードを生成してくれるから悩みはない。しかし、自分でコードを書く場合には、デリゲートとイベントのどちらを使うべきなのか判断に迷うかも知れない。デリゲートとイベントの本質的な違いは何か。

 結論からいうと、イベントの場合は、関数の登録・登録解除のメソッド(addとremove)だけをクラス外に公開し、その他は非公開にするということができるが、デリゲートでは直接にはそれができない。それが本質的な違いである。
 実は、イベントを宣言すると、コンパイラは、privateな隠しデリゲートと、そのデリゲートへの関数の登録・登録解除を行うメソッドであるaddとremove(+= や -= の演算子の呼び出しはこれらのメソッドの呼び出しに変換される)の宣言へとそれを変換する。この隠しデリゲートは常にprivateだが、登録・登録解除メソッドのアクセス(publicかprivateか、など)はイベントに対して宣言されたそれと同じになる
 これらの結果、デリゲートとイベントでは、それを宣言したクラスの外部からのアクセスに違いが生じる。つまり、イベントの場合、例えpublicと宣言しても、クラスの外からはデリゲートそのものは見えず、関数の登録・登録解除を行うメソッドを利用する以外の操作ができない。一方、デリゲートの場合、publicとかprivateとかという指定は、まさにデリゲートそのものへの指定となるから、privateと指定すると、クラスの外からは一切の操作ができなくなる一方、publicと指定すると、クラスの外からであっても、デリゲートに対する操作は、登録・登録解除以外の操作も含め何でもできるようになってしまう。
 デリゲートに対する操作のうち、登録・登録解除以外の操作には、デリゲートの呼び出しと代入がある。つまり、publicなデリゲートではクラスの外からもそのデリゲートを呼び出すことができるし、nullを代入して登録されていた関数を全て削除してしまうこともできる。しかし、クラスの外部からはこのような操作をする必要はない場合が多い。そのような場合にまで、そのような操作を外部に公開することは、オブジェクト指向の本質の一つ、隠蔽の原則に反する。イベントはその対処のためにデリゲートに対する一種のラッパーを自動生成する機能なのである。
 なお、クラス内部からのイベントの呼び出しや代入は、隠しデリゲートへのそれへと変換される。実際には、コンパイラは隠しデリゲートをイベントと同名で生成するようで、クラス内部からのイベントへの操作はデリゲートの操作そのものとなっているようである。

 両者の使い分け方としては、イベントやデリゲートのようなものをpublicで宣言したくなったときには、まずイベントで行けないか検討するのが基本だろう。一方、privateなイベントというのは、以上の趣旨からすれば矛盾した存在である。そういうものを作るくらいなら直接privateなデリゲートを宣言した方が単純で好ましいことが多いだろう。

 なお、やや細かい違いとして、イベントの登録・登録解除のメソッドにはMethodImpl(MethodImplOptions.Synchronized)の属性が付くので、これらの操作がスレッドセーフだという違いもある。privateなイベントというものもこの限りでは存在意義がある。ただし、これらは同期オブジェクトとしてそのメソッドの属するオブジェクト自体を使うので、デッドロックが起こらないよう配慮が必要である。 

 ちなみに、イベントというものが用意された理由としては、歴史的には、Visual Basicのバージョン6.0までにもそれを宣言したクラスからしか呼び出せないイベントという機構が存在し、「次世代Visual Basicランタイム」である.NET Frameworkにもデリゲートだけでなくそれに相当するものが必要だったという事情があったものと思われる。そう考えると、同じ.Net Framework上で開発していても、JavaやC++あたりからC#に移ってきたプログラマと違って、VB6からVB.Netに移行したプログラマはこんなことで悩まないのかも知れない。

余談 - nullチェックの手間を省く

 ところで、デリゲートにしてもイベントにしても、関数の登録・登録解除は変数の中身がnullであっても正常に動作する。ところが、それらを呼び出すときはnullだとNullReferenceExecptionが発生してしまう。そこでMicrosoftは、イベントを呼び出すときは、呼び出しの度に一旦一時変数にデリゲートを代入してからnullでないかどうかチェックせよと言っている(MSDNの『C#プログラミングガイド』の「イベント - イベントの使用」の節参照…2007年11月16日現在、どういうわけかオンライン版の目次に出てこなくなっているようだ)。しかしこれはずいぶん面倒だ。なんとかこれを回避する手段はないか。
 筆者はイベントを宣言する際に次のようにしている。

public event SomeHandler SomeEvent = delegate() { };

あるいは標準のイベントハンドラならば

public event EventHandler SomeStandardEvent = delegate(Object s, EventArgs e) { };

  これだとSomeEvent・SomeStandardEvent変数の内容がnullになることは(クラス内部からわざわざnullを代入しない限り)決してないので、呼び出しの際のnullのチェックを省略できる。つまり呼び出しは単に

SomeEvent();
SomeStandardEvent(this, new EventArgs());

で良い。筆者の知る限りではこれでスレッドセーフでもある。
 イベントでなくデリゲートを生で使っている場合で、それをprivateに指定しなかったときは、いつでも外部からnullにされる可能性があるので、このテクニックは使えない。

参考図書

 デリゲートやイベントについては、次の書籍に比較的詳しく説明されている。本記事でもかなり参考にした。

プログラミングMicrosoft .NET Framework 第2版 (マイクロソフト公式解説書)

 日本語版の題名には.NET Frameworkとしか書かれていないが、原題は『CLR via C#』で、実質C#の本。かなり詳しく書いてある良書。原語では第3版も出ている。著者のJeffrey Richterは『Advanced Windows』でもお馴染み。(Amazonアソシエイト)
追記: 2011年に第3版の邦訳が出た。

追記

 ここまでお読みになった方はうすうすおわかりかとは思うが、この記事は.NET Framework 2.0の頃にその環境を前提に執筆したものであった。ただ、バージョンが4.0まで進もうかという現在でも、イベントとデリゲートの関係はなんら変わっていないはずである。もっとも、今のC#なら、「nullチェックを省く」のところで紹介したダミーの番兵的デリゲートはもっと簡潔に書けるだろう。


コンピュータ関連記事一覧へ