イベントとデリゲート

注意:
この文書は以前「.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プロパティにはこのイベントが発生した時点での時刻が格納されているので、これを表示することでウェイト終了時刻を表示してやります。

 このように、ほとんど同じ結果が得られるにも関わらず、イベントを使う前の一番最初のコードと、イベントを定義した最後のコードでは、全く違うプログラムかのように見えるくらい両者は異なったものになっています。 しかし、デリゲートやイベントはクラスの状態変化を通知したりするのに非常に強力なものになるので、ぜひマスターするといいでしょう。