イベントとデリゲート
注意:
この文書は以前「.NETでいきまっしょい!」で公開していたものですが、公開以降メンテナンスされていません。 今や古い情報となった内容が記載されている場合があるのでご注意ください。
1.イベントとデリゲート、その前に
Visual Basicではコマンドボタンがクリックされたときとか、テキストボックスに文字が入力されたときなどにイベントとしてメッセージが通知されてきました。 Visual Basic .NETでも同様のイベントというものが存在します。 また、Visual Basicでのクラス同様、独自のイベントを定義することができます。 が、Visual Basic .NETになってからイベントにまつわる部分が大きく変わりました。 個人的には.NETのイベントの方がわかりやすくなったかと思います(VBではほとんど使わなかったからかもしれません)。 ここではこのイベントと、それと切っても切り離せない関係にあるデリゲートについて考えてみたいと思います。
まず、イベント・デリゲートをいきなり使う前に、次のコードを作ってみました。
指定した時間だけウェイトする
001 002 003 004 005 006 007 008 009 010
011 012 013 014 015 016 017 018 019 020
021 022 023 024 025 026 027 028 029 030
031 032 033 034 035
|
Imports System
Imports System.Threading
Module Main
' アプリケーションのエントリーポイント
Public Function Main(ByVal args() As String) As Integer
Console.WriteLine("Start: {0}", DateTime.Now)
Wait(3000)
Console.WriteLine("Finished: {0}", DateTime.Now)
Return 0
End Function
' 指定されたミリ秒だけウェイトする
Public Sub Wait(ByVal millisec As Integer)
Thread.Sleep(millisec)
WaitOver()
End Sub
' ウェイトが終了したことを知らせる
Public Sub WaitOver()
Console.WriteLine("Wait over.")
End Sub
End Module
|
| 出力結果 |
Start: 2003/02/21 0:00:39
Wait over.
Finished: 2003/02/21 0:00:42
Press any key to continue
|
ソースコードと実行結果とを見比べてもらえばすぐにわかりますが、複雑なことは何もやっていません。 63行目のThread.Sleep()は指定されたミリ秒だけ現在のスレッドを停止するというメソッドです。 Main()メソッドではWait()メソッドを呼び出す前後に、現在の時刻を表示するようにしています。 また、このサンプルでは3000ミリ秒、つまり3秒の間ウェイトするようにしています。 実行結果を見ればわかるように、始まりと終わりの間がちゃんと3秒になっています。
つぎに、このソースコードをデリゲートを使ったものに書き換えることにします。
2.デリゲート
「イベントとデリゲート」という主題通り、まずイベントをやるのかと思っていた方には申し訳ありませんが、まず最初にデリゲートについて考えてみることにします。 主題を順番通り「デリゲートとイベント」にすればいいのですが、語感が悪いので「イベントと〜」にしました(内部事情)
まず、この文章を読んで勉強しようと思われる方の多くは、「イベントは知っているけどデリゲートなんて聞いたことがない」というような方だと思います。 当初僕自身も、デリゲートが理解不能でした。 C++を勉強した方の中には「関数ポインタ」というものをご存じの方もいると思いますが、デリゲートの役割はこの関数ポインタに似ているものといえます(厳密には異なるものです)。
というのも、デリゲートはあるメソッド(共有メソッド及びインスタンスメソッド)を特定する役割を持っているためです。 つまりその役割は、直接特定のメソッドを呼び出すのではなく、デリゲートによってそのデリゲートに指定されているメソッドを間接的に呼び出すことができるわけです。 イベントを発生するときに呼び出すべきメソッドをするためにデリゲートが使われます。 言い換えると、イベントはデリゲートに指定されているメソッドを呼び出します。
ここではまずイベントを用いず、先ほどのサンプルをデリゲートだけを用いたサンプルに作り替えてみます。
指定した時間だけウェイトする(デリゲート使用)
001 002 003 004 005 006 007 008 009 010
011 012 013 014 015 016 017 018 019 020
021 022 023 024 025 026 027 028 029 030
031 032 033 034 035 036 037 038 039 040
041 042
|
Imports System
Imports System.Threading
Module Main
Public Delegate Sub WaitOverDelegate()
Dim WaitOverMethod As WaitOverDelegate
' アプリケーションのエントリーポイント
Public Function Main(ByVal args() As String) As Integer
Console.WriteLine("Start: {0}", DateTime.Now)
'WaitOverMethod = AddressOf WaitOver
WaitOverMethod = New WaitOverDelegate(AddressOf WaitOver)
Wait(3000)
Console.WriteLine("Finished: {0}", DateTime.Now)
Return 0
End Function
' 指定されたミリ秒だけウェイトする
Public Sub Wait(ByVal millisec As Integer)
Thread.Sleep(millisec)
WaitOverMethod()
End Sub
' ウェイトが終了したことを知らせる
Public Sub WaitOver()
Console.WriteLine("Wait over.")
End Sub
End Module
|
| 出力結果 |
Start: 2003/02/21 0:00:39
Wait over.
Finished: 2003/02/21 0:00:42
Press any key to continue
|
早速デリゲートが出てきましたが、落ち着いてみてみましょう。 このサンプルではWaitOverメソッドをデリゲートにより呼び出すことを試みています。 まず、6行目では新しいデリゲート型の宣言を行っています。 デリゲートではどんなメソッドでも呼び出せるわけではなく、引数・戻り値が一致するメソッドだけを呼び出せるので、そのデリゲートがどのようなメソッドを呼び出せるかを定義します。 ここでは、戻り値無し、引数無しのメソッドをWaitOverDelegateとしています。
続いて8行目はたった今宣言したデリゲート型のインスタンスを格納するために、WaitOverDelegate型のオブジェクト変数WaitOverMethodを宣言しています。 WaitOverDelegate型とおなじ形式のメソッドを呼び出すためにはこの変数に呼び出したいメソッドを指定します。 実際に指定しているのが15・16行目です。 コメント化されている部分は、このコードでも意味は変わらないということを示しているので、どちらか好きな方でコーディングすることができます。 このコードのようにデリゲート型の変数に呼び出させたいメソッドを指定するには、「AddressOf (対象のメソッド)」という風に指定してやります。 ここで、対象のメソッドは、引数・戻り値がデリゲート型で指定されているものと一致する共有メソッドまたはアクセス権のあるインスタンスメソッドです。
さて、デリゲート型の変数に呼び出させたいメソッドを指定することができたら、後は呼び出すだけです。 実際に呼び出しを行っているのが29行目です。 一見WaitOverMethod()というメソッドを呼び出しているように見えますが、実際にはデリゲート型変数WaitOverMethod()に指定されているメソッドであるWaitOver()が呼び出されています。 ですから、直接WaitOver()メソッドを呼び出すようなコードは記述されていなくても、デリゲートが間接的に呼び出してくれるわけです。
このサンプルをさらに改変して、インスタンスのメソッドも呼び出せるかどうかを実際に見てみます。
3.インスタンスメソッドを呼び出す
前のサンプルをクラス化して改変を加えたものが次のソースコードです。 ざっとコードの概要を説明しますと、今回使用しているデリゲート型WaitOverDelegateは、各クラス共通に用いられるのでPublicとし、クラス外で宣言します。
さらに、機能別に三つのクラスに分割しました。 MainクラスはアプリケーションのエントリーポイントとしてのMainメソッドを提供し、プログラムの流れを記述します。 WaitExecuterクラスはウェイトを実行するためのクラスで、ウェイトが完了するとWaitOverMethodで指定されているメソッドを呼び出します。 WaitOverNotifierクラスは、ウェイトが終了したことを通知するためのクラスです。
指定した時間だけウェイトする(クラス化)
001 002 003 004 005 006 007 008 009 010
011 012 013 014 015 016 017 018 019 020
021 022 023 024 025 026 027 028 029 030
031 032 033 034 035 036 037 038 039 040
041 042 043 044 045 046 047 048 049 050
051 052 053 054 055 056 057
|
Imports System
Imports System.Threading
' デリゲート型の宣言
Public Delegate Sub WaitOverDelegate()
Public Class Main
' アプリケーションのエントリーポイント
Public Shared Function Main(ByVal args() As String) As Integer
Dim e As New WaitExecuter()
Dim n As New WaitOverNotifier()
' ウェイトが終了したことを通知するインスタンスメソッドを指定
e.WaitOverMethod = AddressOf n.WaitOver
Console.WriteLine("Start: {0}", DateTime.Now)
e.Wait(3000)
Console.WriteLine("Finished: {0}", DateTime.Now)
Return 0
End Function
End Class
' ウェイトを行うためのクラス
Public Class WaitExecuter
Public WaitOverMethod As WaitOverDelegate
' 指定されたミリ秒だけウェイトする
Public Sub Wait(ByVal millisec As Integer)
Thread.Sleep(millisec)
' ウェイト終了を通知
WaitOverMethod()
End Sub
End Class
' ウェイト終了を感知するためのクラス
Public Class WaitOverNotifier
' ウェイトが終了したことを知らせる
Public Sub WaitOver()
Console.WriteLine("Wait over.")
End Sub
End Class
|
| 出力結果 |
Start: 2003/02/21 2:40:06
Wait over.
Finished: 2003/02/21 2:40:09
Press any key to continue
|
見ての通り実行結果は予想通りで、インスタンスメソッドでもデリゲートはちゃんと機能します。 参考までに、デリゲートは異なるインスタンスのメソッドを区別します。 つまり、インスタンスAとインスタンスBのメソッドMは別のものとして扱われます。
次はイベントを定義することにします。
4.イベント
さて、ここでやっとなじみ深いイベントが出てくるわけです。 イベントはクラス内の状態の変化などを外部に伝える役割を持っています。 ところで、デリゲートはメソッドを特定する役割を持つとしました。 イベントは外部に情報を伝えるためにありますが、伝えるための各々のメソッド、つまりイベントハンドラはどんなメソッドでもいいわけではなく、引数や戻り値がすべて一致している必要があります。 ここで、イベントハンドラの型を定義するためにデリゲート型を用います。
説明だけではよくわからないと思うので、実際にイベントとイベントハンドラを用いたサンプルを次に示します。 例によって、前のサンプルを少し改変したものです。
指定した時間だけウェイトする(イベント使用)
001 002 003 004 005 006 007 008 009 010
011 012 013 014 015 016 017 018 019 020
021 022 023 024 025 026 027 028 029 030
031 032 033 034 035 036 037 038 039 040
041 042 043 044 045 046 047 048 049 050
051 052 053 054 055 056 057 058
|
Imports System
Imports System.Threading
' デリゲート型の宣言
Public Delegate Sub WaitOverDelegate()
Public Class Main
' アプリケーションのエントリーポイント
Public Shared Function Main(ByVal args() As String) As Integer
Dim e As New WaitExecuter()
Dim n As New WaitOverNotifier()
' ウェイトが終了したことを通知するイベントハンドラを指定
AddHandler e.WaitOver, AddressOf n.WaitOver
Console.WriteLine("Start: {0}", DateTime.Now)
e.Wait(3000)
Console.WriteLine("Finished: {0}", DateTime.Now)
Return 0
End Function
End Class
' ウェイトを行うためのクラス
Public Class WaitExecuter
' ウェイトが終了したことを通知するイベント
Public Event WaitOver As WaitOverDelegate
' 指定されたミリ秒だけウェイトする
Public Sub Wait(ByVal millisec As Integer)
Thread.Sleep(millisec)
' ウェイト終了イベントを発生
RaiseEvent WaitOver()
End Sub
End Class
' ウェイト終了を感知するためのクラス
Public Class WaitOverNotifier
' ウェイトが終了したことを知らせる
Public Sub WaitOver()
Console.WriteLine("Wait over.")
End Sub
End Class
|
| 出力結果 |
Start: 2003/02/21 3:05:40
Wait over.
Finished: 2003/02/21 3:05:43
Press any key to continue
|
まず、このコードで大きく変わったところから説明していきます。 まず、32行目ですが、以前はデリゲート型のパブリック変数が宣言されていましたが、ここではEventキーワードによりイベントとして宣言されています。 また、「Event (イベント名) As (デリゲート型)」とすることで、このイベントのハンドラはデリゲート型で指定されている形式と同じ形式を持たなければならなくなります。
イベント宣言へと変わったことで、16行目も変わりました。 AddHandlerステートメントは、イベントに適切なイベントハンドラを追加するためのものです。 参考までに、イベントのリストから削除するためにRemoveEventステートメントも用意されています。 AddHandlerステートメントは「AddHandler (イベント), AddressOf (イベントハンドラ)」のように記述します。 これにより16行目では、e.WaitOverイベントにn.WaitOver()をイベントハンドラとして追加することができます。
最後に、デリゲートの時はメソッド同様に呼び出すことができましたが、イベントの場合は異なります。 40行目にあるように、RaiseEventステートメントでイベントを発生させます。 このとき、WaitOverに指定されている全てのイベントハンドラが呼び出されます。
この例ではイベントハンドラを一つしか設定しませんでしたが、一つのイベントに対して複数のイベントハンドラを設定することも可能です。 また、イベントも同様に複数定義できます。 この次では、イベントに引数を持たせることでクラス内の情報を詳細に伝えることを考えてみます。
5.イベントの引数
ただ単にイベントハンドラを呼び出しただけでは、そのイベントが発生したときのクラス内の状態を知ることができません。 そのため、イベントに引数を持たせることを考えてみます。 VB.NETのコントロールなどのほとんどのイベントハンドラでは、イベントを発生させたオブジェクトを知るための sender と、そのイベントが発生した時点での状況などを記した e が、イベントハンドラの引数として取得できます。 具体的には sender は Object型、e は EventArgsクラスを継承した型です。
このサンプルでも、その慣例に従って、EventArgsクラスを継承したクラスWaitOverEventArgsをイベントの引数として渡してみることにします。 このクラスにはウェイトが完了した時刻を格納させるようにします。 また、イベントに引数を持たせるため、デリゲートも多少変更を加え、名前もWaitOverEventHandlerに変えます。
指定した時間だけウェイトする(引数付き)
001 002 003 004 005 006 007 008 009 010
011 012 013 014 015 016 017 018 019 020
021 022 023 024 025 026 027 028 029 030
031 032 033 034 035 036 037 038 039 040
041 042 043 044 045 046 047 048 049 050
051 052 053 054 055 056 057 058 059 060
061 062 063 064 065 066 067 068 069 070
071 072 073 074 075 076 077 078 079 080
081 082 083 084
|
Imports System
Imports System.Threading
' デリゲート型の宣言
Public Delegate Sub WaitOverEventHandler(ByVal sender As Object, ByVal e As WaitOverEventArgs)
Public Class Main
' アプリケーションのエントリーポイント
Public Shared Function Main(ByVal args() As String) As Integer
Dim e As New WaitExecuter()
Dim n As New WaitOverNotifier()
' ウェイトが終了したことを通知するイベントハンドラを指定
AddHandler e.WaitOver, AddressOf n.WaitOver
Console.WriteLine("Start: {0}", DateTime.Now)
e.Wait(3000)
Return 0
End Function
End Class
' ウェイトが終了したときのイベント引数のためのクラス
Public Class WaitOverEventArgs
' EventArgsを継承
Inherits EventArgs
' ウェイトが終了したときの時間
Protected m_dtm As DateTime
' 読み取り専用プロパティ
Public ReadOnly Property WaitOverTime() As DateTime
Get
Return m_dtm
End Get
End Property
' コンストラクタ
Public Sub New(ByVal dtm As DateTime)
MyBase.New()
m_dtm = dtm
End Sub
End Class
' ウェイトを行うためのクラス
Public Class WaitExecuter
' ウェイトが終了したことを通知するイベント
Public Event WaitOver As WaitOverEventHandler
' 指定されたミリ秒だけウェイトする
Public Sub Wait(ByVal millisec As Integer)
Thread.Sleep(millisec)
' ウェイト終了イベントを発生
RaiseEvent WaitOver(Me, New WaitOverEventArgs(DateTime.Now))
End Sub
End Class
' ウェイト終了を感知するためのクラス
Public Class WaitOverNotifier
' ウェイトが終了したことを知らせる
Public Sub WaitOver(ByVal sender As Object, ByVal e As WaitOverEventArgs)
' ウェイト終了時間を出力する
Console.WriteLine("Wait over, {0}.", e.WaitOverTime)
End Sub
End Class
|
| 出力結果 |
Start: 2003/02/21 3:51:39
Wait over, 2003/02/21 3:51:42.
Press any key to continue
|
まず、イベント引数のためのクラスWaitOverEventArgsが新しく定義されました。 このクラスはイベントが発生した時間を保持する以外の役割はありません。 64行目のRaiseEventステートメントでは、WaitOverイベントのデリゲート型が引数をとるように変わったので、それに伴い二つの引数を渡しています。 ここで、新しく作成されたWaitOverEventArgsを渡すことで、イベントが発生した時刻を知らせるわけです。 また、senderにMeを渡すことで、イベントを発生させた張本人である自分自身をイベントハンドラに通知します。
さらに、イベントハンドラの方も変更を加えます。 75行目のWaitOverイベントハンドラも、デリゲート型のパラメータ形式が変わったのでそれに伴い形式を変更します。 ここで、このイベントハンドラが呼び出されたとき、引数として渡されるeのWaitOverTimeプロパティにはこのイベントが発生した時点での時刻が格納されているので、これを表示することでウェイト終了時刻を表示してやります。
このように、ほとんど同じ結果が得られるにも関わらず、イベントを使う前の一番最初のコードと、イベントを定義した最後のコードでは、全く違うプログラムかのように見えるくらい両者は異なったものになっています。 しかし、デリゲートやイベントはクラスの状態変化を通知したりするのに非常に強力なものになるので、ぜひマスターするといいでしょう。