VB のたまご

作成日: 2017/10/01, 更新日: 2017/10/01


イベントとデリゲート(2017年10月版)

  •  イベントとデリゲートの役割について、改めて書き直しましたので共有します。

  •  確認環境
  •  ・Visual Studio Community 2015
  •  ・.NET Framework 4.5.2

イベントとは何か?(イベント機構とイベント用のメソッド、その間を取り持つデリゲート「イベントハンドラ」)

  •  Visual Studio で作ることができる画面を持つアプリケーションに、Windows Forms アプリケーションがあります。 Windows Forms アプリケーションでは、画面デザインでボタンやテキストボックスを配置して、 画面が表示された、ボタンが押されたなど、それぞれのイベントが発生したときに動作させたい処理をメソッドに書いていきます。

  •  例えば、画面が表示されたときに発生するイベント「Form.Shown イベント」には、動作させたいメソッドとして「Form1_Shown メソッド」に書いていきますし、 ボタンを押したときに発生するイベント「Button.Click イベント」には、動作させたいメソッドとして「Button1_Click メソッド」に書いていきます。

  •  ただし、このままだとイベントとメソッドが関連づいていることが判断できないため、 Handles で静的に関連付けたり、AddHandler / RemoveHandler で動的に関連付けたり解除したりします。

  •  Form1.vb(あらかじめ、ボタンを配置している)
  • Public Class Form1
    
        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click ' ★← Handles で関連付ける
            ' ~ここにやりたい処理を書いていく~
        End Sub
    
    End Class
    

  •  ここで、サンプルとして Button クラスのクリックイベントを見てみましょう。クラスメンバーとして持っているはずです。 画面デザイン用に分離されている Form1.Designer.vb を開いて見ると、以下のように InitializeComponent メソッドと Dispose メソッドが定義されており、 各コントロールの宣言や初期値の設定が命令されています。
  • <Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
    Partial Class Form1
        Inherits System.Windows.Forms.Form
    
        'フォームがコンポーネントの一覧をクリーンアップするために dispose をオーバーライドします。
        <System.Diagnostics.DebuggerNonUserCode()> _
        Protected Overrides Sub Dispose(ByVal disposing As Boolean)
            ' ~省略~
        End Sub
    
        'Windows フォーム デザイナーで必要です。
        Private components As System.ComponentModel.IContainer
    
        'メモ: 以下のプロシージャは Windows フォーム デザイナーで必要です。
        'Windows フォーム デザイナーを使用して変更できます。  
        'コード エディターを使って変更しないでください。
        <System.Diagnostics.DebuggerStepThrough()> _
        Private Sub InitializeComponent()
            Me.Button1 = New System.Windows.Forms.Button()
    
            ' ~省略~
    
        End Sub
    
        Friend WithEvents Button1 As Button ' ★←「Button」の文字列上で右クリック→「定義に移動」
    
    End Class
    

  •  このクラスの一番最後辺りにコントロールをフィールドメンバーとして定義している個所がありますが、 ここでコントロールの型を選択して、右クリック→「定義に移動」をクリックします。

  •  すると、Button [メタデータから] というタブが表示されます(おそらく dllをリフレクションして疑似ソースコードとして作った)。 ※コメントや全メンバーは分かりづらいため消しています。
  • Namespace System.Windows.Forms
    
        Public Class Button
            Inherits ButtonBase
            Implements IButtonControl
    
            '~いろいろ省略~
    
            '
            ' 概要:
            '     ユーザーが System.Windows.Forms.Button コントロールをダブルクリックすると発生します。
            Public Event DoubleClick As EventHandler
            '
            ' 概要:
            '     ユーザーがマウスで System.Windows.Forms.Button コントロールをダブルクリックすると発生します。
            Public Event MouseDoubleClick As MouseEventHandler
            
        End Class
    End Namespace
    

  •  ここで Click イベントが定義されていると思いきや、見当たりません。無いということは継承元クラス側に定義されているということです。 今度は、ButtonBase クラスを選択して右クリック→「定義に移動」をクリックします。

  •  表示された ButtonBase クラスですが、残念なことにここにも Click イベントは見当たらないため、 継承元クラスとなる Control クラスを選択して右クリック→「定義に移動」をクリックします。
  • Namespace System.Windows.Forms
        Public Class Control
            Inherits Component
            Implements IDropTarget, ISynchronizeInvoke, IWin32Window, IArrangedElement, IBindableComponent, IComponent, IDisposable
    
            ' ~いろいろ省略~
            
            '
            ' 概要:
            '     コントロールがクリックされたときに発生します。
            Public Event Click As EventHandler
            
            ' ~いろいろ省略~
    
        End Class
    End Namespace
    

  •  Control クラスにはありましたね!Click イベント!

  •  ここで注目してほしいのは、Event キーワードとクラスの型である「EventHandler」型です。 再度、EventHandler の型を選択して、右クリック→「定義に移動」を押します。
  • Namespace System
        '
        ' 概要:
        '     イベント データを持たないイベントを処理するメソッドを表します。
        '
        ' パラメーター:
        '   sender:
        '     イベントのソース。
        '
        '   e:
        '     イベント データを含まないオブジェクト。
        <ComVisible(True)>
        Public Delegate Sub EventHandler(sender As Object, e As EventArgs)
    End Namespace
    

  •  この EventHandler はクラスではなくデリゲートなんです。

  •  ちょっとここで、おさらいです。

  •  Button クラスの Click イベントを見てみたら、

  •  Button クラスの Click イベントメンバーは、EventHandler デリゲート型でした。

  •  ちょっと気になる点として、処理したいメソッド(Button1_Click)と、EventHandler デリゲート型が、見た目そっくりだという点です。
  • Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Public Delegate Sub EventHandler(sender As Object, e As EventArgs)
    

  •  戻り値無しの Sub メソッドで、第一引数が Object 型、第二引数が EventArgs 型ですね!


デリゲートとは何か?

  •  仕組み的に、イベントは突き詰めていくとデリゲートであるということが分かりました。(ただし、これがどういうことを意味しているのかは、まだ分かりません)。 イベントを知るためには、デリゲートを知る必要があるみたいです。

  •  昔はプログラマーが自分でデリゲートを宣言して使うという手間があり、ここですでに使いづらい・意味が分からない、という印象を与えてしまいましたが、 現在では .NET Framework 側で準備されているジェネリックデリゲートを使うことである程度簡単に扱えるようになりました。 ジェネリック型という後から型を決定する仕組みについては、ここでは本題から外れるため、あらかじめ周知されている前提で進めます。

  •  以下は、ジェネリックデリゲートのサンプルです。

  •  デリゲートのインスタンス内でメソッド名を指定するのですが、この時に AddressOf キーワードを付けるのがルールです。 【Address(住所) Of(~の) メソッド名】と言うからには、多分メソッド自体ではなく、メソッドを定義している住所(メモリ位置)をセットしているのだと思います。
  • Private Sub Test()
    
        ' Sub で引数無しメソッド
        Dim ac1a As Action = New Action(AddressOf Aaa) ' メソッドを変数扱いする
        Dim ac1b As Action = AddressOf Aaa             ' 変数の型を明記した場合は、Action のインスタンス明記は、省略可能
    
        ' 任意のタイミングで、そのメソッドを実行
        ac1a()        ' Hello!
        ac1b.Invoke() ' Hello!
    
    
    
        ' Sub で 第一引数が String 型のメソッド
        Dim ac2 As Action(Of String) = New Action(Of String)(AddressOf Bbb)
    
        ac2("VB.NET!")        ' VB.NET!
        ac2.Invoke("VB.NET!") ' VB.NET!
    
    
    
        ' Sub で、第一引数が Object 型、第二引数が EventArgs 型のメソッド
        Dim ac3 As EventHandler = New EventHandler(AddressOf Ccc)
        Dim ac4 As EventHandler = AddressOf Ccc
    
        ' 戻り値と引数の数、引数の型が全て一致したとしても、デリゲート型が違えば別物と判断される
        'Dim ac5 As Action(Of Object, EventArgs) = New EventHandler(AddressOf Ccc)
        Dim ac5 As Action(Of Object, EventArgs) = AddressOf Ccc
    
        ac3(Nothing, Nothing) ' よくあるイベントの形1
        ac4(Nothing, Nothing) ' よくあるイベントの形1
        ac5(Nothing, Nothing) ' よくあるイベントの形1
    
    
    
        ' Sub で、第一引数が Object 型、第二引数が MouseEventArgs 型のメソッド
        Dim ac6 As EventHandler(Of MouseEventArgs) = AddressOf Ddd
        Dim ac7 As MouseEventHandler = AddressOf Ddd
    
        ' 戻り値と引数の数、引数の型が全て一致したとしても、デリゲート型が違えば別物と判断される
        'Dim ac8 As Action(Of Object, MouseEventArgs) = New EventHandler(Of MouseEventArgs)(AddressOf Ddd)
        'Dim ac8 As Action(Of Object, MouseEventArgs) = New MouseEventHandler(AddressOf Ddd)
        Dim ac8 As Action(Of Object, MouseEventArgs) = AddressOf Ddd
    
        ac6(Nothing, Nothing) ' よくあるイベントの形2
        ac7(Nothing, Nothing) ' よくあるイベントの形2
        ac8(Nothing, Nothing) ' よくあるイベントの形2
    
    End Sub
    
    Private Sub Aaa()
        Console.WriteLine("Hello!")
    End Sub
    
    Private Sub Bbb(message As String)
        Console.WriteLine(message)
    End Sub
    
    Private Sub Ccc(sender As Object, e As EventArgs)
        Console.WriteLine("よくあるイベントの形1")
    End Sub
    
    Private Sub Ddd(sender As Object, e As MouseEventArgs)
        Console.WriteLine("よくあるイベントの形2")
    End Sub
    

  •  汎用型のデリゲートとして、Sub なメソッドを担当する Action デリゲート、Function なメソッドを担当する Func デリゲートがあります。 さらに、初期からある EventHandler デリゲートや XxxEventHandler デリゲートもありますし、EventHandler 用の汎用型デリゲートもあります。

  •  ジェネリック型の恩恵が受けられる今となっては、見返してみると、多種多様なデリゲートは混乱するだけの機能改悪な気がします。 特に、同じ戻り値・同じ引数がかぶってしまうデリゲートは、どれを使えばいいのかプログラマーを迷わせてしまいます。

  •  デリゲートは、変数にメソッドをセットすることができて、任意のタイミングでそのメソッドを実行することができます。 本来メソッドは呼び出すものであって、持ち回るものではないのですが、デリゲートにセットすると、変数扱いとメソッド実行することができるようになります。

  •  別に、デリゲートを介さなくても、任意の場所でメソッドを呼び出せばいいことなので、 わざわざデリゲート経由で任意のタイミングで実行しなくてもいいと思われることと思います。
  • Private Sub Test2()
    
        Dim fc = New Func(Of Integer, Integer, Integer)(AddressOf Plus)
        Dim x = 0
    
        ' ある処理1
        x = fc(1, 2)    ' x = 3
        x = Plus(1, 2)  ' x = 3
    
        ' ある処理2
        x = fc(1, 2)    ' x = 3
        x = Plus(1, 2)  ' x = 3
    
        ' ある処理3
        x = fc(1, 2)    ' x = 3
        x = Plus(1, 2)  ' x = 3
    
    End Sub
    
    Private Function Plus(i1 As Integer, i2 As Integer) As Integer
        Return i1 + i2
    End Function
    

  •  確かにその通りです、どちらも同じことです。

  •  想定する場面としては、デリゲートを利用して、メソッドの処理内容を切り替えて使いたい。という場面があります。 ちょっと以下も苦しいサンプルになってしまいましたが、イメージはこんな感じです。 メソッド(をセットしたデリゲート変数)を引数にセットして、渡したメソッド内で、メソッド(をセットしたデリゲート変数)を実行させる。という考え方です。
  • Private Sub Test3()
    
        Dim x As Integer = 0
    
        ' 足し算したい
        x = Calc(AddressOf Plus, 1, 2) ' x = 3
        x = Plus(1, 2)                 ' x = 3
    
        ' 引き算したい
        x = Calc(AddressOf Minus, 1, 2) ' x = -1
        x = Minus(1, 2)                 ' x = -1
    
    End Sub
    
    ' 処理を柔軟に切り替えたい
    ' 足し算もできるし、引き算もできる処理にしたい
    Private Function Calc(fc As Func(Of Integer, Integer, Integer),
                          i1 As Integer,
                          i2 As Integer) As Integer
        Return fc(i1, i2)
    End Function
    
    Private Function Plus(i1 As Integer, i2 As Integer) As Integer
        Return i1 + i2
    End Function
    
    Private Function Minus(i1 As Integer, i2 As Integer) As Integer
        Return i1 - i2
    End Function
    

  •  これは多分イメージは伝わると思いますが、やはり依然として Calc メソッド経由にしなくても直接 Plus メソッドや Minus メソッドを呼び出せばいいじゃん。となりますよね。 ぶっちゃけると、ラムダ式との組み合わせで効果を発揮します。
  • Private Sub Test4()
    
        Dim x As Integer = 0
    
        ' 足し算したい
        x = Calc(Function(i1, i2) i1 + i2, 1, 2) ' x = 3
    
        ' 引き算したい
        x = Calc(Function(i1, i2) i1 - i2, 1, 2) ' x = -1
    
    End Sub
    
    ' 処理を柔軟に切り替えたい
    ' 足し算もできるし、引き算もできる処理にしたい
    Private Function Calc(fc As Func(Of Integer, Integer, Integer),
                          i1 As Integer,
                          i2 As Integer) As Integer
        Return fc(i1, i2)
    End Function
    

  •  ちょっと脱線してしまいました。デリゲートにラムダ式(メソッド定義無しで、即席メソッドを作れる)をセットすることで、メソッドを持ち回るサンプルでした。 これらに関しては、今は意味不明のままで大丈夫です。ラムダ式は別途説明したいと思います。

  •  とにかく、【デリゲートはメソッドを包み込むことができるもの】と考えることができます。当然、メソッドをセットしていないのに実行すると例外エラーになります。
  • Private Sub Test5()
    
        ' Sub で引数無しメソッド
        Dim ac As Action = Nothing
    
        'ac()
        ' 型 'System.NullReferenceException' のハンドルされていない例外が WindowsApplication1.exe で発生しました
        ' 追加情報:オブジェクト参照がオブジェクト インスタンスに設定されていません。
    
    End Sub
    


デリゲートだけで、イベント機構を作れないか?

  •  Button クラスの Click イベントは、メソッドを関連付けていればそのメソッドが実行されるし、何も関連付けていなければ何も起きないだけです。 これならば、Event キーワードを使わなくてもイベント機構を作れそうですね!
  • ' イベントを提供する側
    Class MyButton
    
        Public Click As Action(Of Object, EventArgs) = Nothing
    
        Public Sub PerformClick()
    
            If Click IsNot Nothing Then
                Me.Click(Me, EventArgs.Empty)
            End If
    
        End Sub
    
    End Class
    
    ' イベントを購読する側
    Class MyForm
    
        Sub Main()
    
            Dim button1 As MyButton = New MyButton
            button1.Click = AddressOf MyButton_Click
            button1.PerformClick()
    
        End Sub
    
        Sub MyButton_Click(sender As Object, e As EventArgs)
    
            Dim sf = New StackFrame(1)
            Dim methodName = sf.GetMethod().ReflectedType.Name ' sf.GetMethod().Name
            Console.WriteLine($"{methodName} から呼ばれました")
    
        End Sub
    
    End Class
    

  •  おお!できました!デリゲートだけで事足りますね!


イベントとは何か?(イベント定義したメンバーは、型が何らかのデリゲートになっている)

  •  もしもデリゲートだけで済むのであれば、Event というキーワードは生まれなかったはずです。 ということは何か理由がありそうですね。今度は、デリゲート版イベントとイベント版イベントの比較を見てみましょう。
  • ' イベントを購読する側
    Class MyForm
    
        Sub Main()
    
            Dim button1 As MyButton = New MyButton
            button1.Click = AddressOf MyButton_Click
            button1.PerformClick() ' PerformClick メソッド内では Nothing チェック済み
            button1.Click(Me, EventArgs.Empty) ' デリゲートは直接呼び出せる、でももし、メソッドが関連付けられていなかったら例外エラーが発生する
            ' もしも、メソッドが関連付けられていない状態で、デリゲートを直接呼び出してしまうと・・・
            ' 型 'System.NullReferenceException' のハンドルされていない例外が WindowsApplication1.exe で発生しました
            ' 追加情報:オブジェクト参照がオブジェクト インスタンスに設定されていません。
    
            Dim button2 As Button = New System.Windows.Forms.Button
            AddHandler button2.Click, AddressOf MyButton_Click
            button2.PerformClick()
            'button2.Click(Me, EventArgs.Empty) ' イベントは直接呼び出せない
            ' こう書こうとするとコンパイルエラーとして教えてくれる
            ' コントロールがクリックされたときに発生します。
            ' 'Public Event Click As EventHandler' はイベントであるため、直接呼び出すことはできません。
            ' イベントを発生させるには 'RaiseEvent' ステートメントを使用してください。
    
        End Sub
    
    End Class
    

  •  イベント機構として考える場合、デリゲートのままでの問題点が、【直接デリゲートを呼び出せる】ことです。 もし、関連付けるメソッドがセットされていない状態で、デリゲートを直接呼び出してしまうとどうなるでしょう? 先ほどのサンプルのように、例外エラーが発生してしまいます。

  •  プログラマーに、「デリゲートを呼び出す時は、関連付けるメソッドをセットし忘れないでね、必ず守ってね」などと周知するようでは、イベント機構としては不完全です。 プログラマーには、(アプリケーション開発には関係ない、VB.NET 言語側の)余計なルールは順守させるべきではありません。

  •  言語提供側としては、危ない操作ができてしまう可能性があるプログラミングの書き方は、誘導させない・避けるべきことなので、 デリゲートをイベント機構として扱えるように、制約を付け加えたものとして、イベントが生まれたのだと考えるのが自然です。

  •  比較対象として System.Windows.Forms.Button クラスで同じことをしてみます。 こちらは、イベントを直接呼び出すことができません。よって、画面操作からのイベント発生を待つしかないですね。 (クリックを疑似的に発生させる PerformClick メソッドが提供されていますが、特殊なだけで、ほとんどの他のイベントには疑似発生メソッドはありません)。

  •  これにより、【画面が表示されたとき】に、関連づいていれば【画面表示の処理】が実行されますし、 【ボタンを押したとき】に、関連づいていれば【ボタンを押したときの処理】が実行されるという、自然な流れとして受け入れることができます。 【関連づいていなければ、何もしない】、という動作が大事な事です。


イベントの作り方・使い方

  •  それでは、イベントを提供する側、イベントを購読する側に分けて、イベントを作ってみましょう。

  •  最初にイベントを提供する側です。ここではイベントを提供するために、Event を定義して公開します。 イベントを定義する際は、【イベント名 As デリゲート型】という書き方と、【イベント名(引数1、引数2、、)】という書き方の2つがあります。 後者はイベント名とデリゲート型を合わせた宣言の仕方です。ネットを見ていると、型がはっきりしている【イベント名 As デリゲート型】という書き方が多いですね。

  •  続いて、イベントを発生させるためのメソッドを作成します。このメソッド名には慣例として、On + イベント名という形式が多く使われています。 順守しなくてもいいはずですが、例えば .NET Framework に含まれている Button, Control クラスなどでは、このような命名規則に従って実装しています。

  •  このメソッド内で、【RaiseEvent イベント名】という命令を呼び出すことで、イベントを発生させることができます。
  • ' イベントを提供する側
    Class MyNotifier
    
        Public Event Aaa As Action
        Public Event Bbb As EventHandler
    
        Public Event Ccc(s As String)
        Public Event Ddd(sender As Object, e As MouseEventArgs)
    
        Public Sub OnAllEvent()
    
            RaiseEvent Aaa()
            RaiseEvent Bbb(Me, EventArgs.Empty)
            RaiseEvent Ccc("MyNotifier クラスからの通知です")
    
            Dim arg = New MouseEventArgs(MouseButtons.Left, 1, 50, 50, 1)
            RaiseEvent Ddd(Me, arg)
    
        End Sub
    
        Public Sub OnCcc(message As String)
            RaiseEvent Ccc(message)
        End Sub
    
        Public Sub OnAaa()
            RaiseEvent Aaa()
        End Sub
    
    End Class
    

  •  続いて、イベントを使う側です。

  •  変数宣言は通常のクラスと同じようにできるのですが、特殊な命令として WithEvents があります。 これを付けることで、Handles の対象として扱うことができるようになります。静的な関連付け方法です。

  •  逆に、動的な関連付け方法として、AddHandler があります。【AddHandler イベント名, メソッド名】という書き方で、 引数の数や型が一致しているものだけを関連付けることができます。
  • ' イベントを使う側
    Class MyReceiver
    
        Private WithEvents notifier1 As MyNotifier = New MyNotifier
        Private notifier2 As MyNotifier = New MyNotifier
    
        Public Sub New()
    
            ' Handles の効果により、notifier1 の Ccc イベントに2つのメソッドを、
            ' notifier2 の Ccc イベントに1つのメソッドを関連付ける
            AddHandler Me.notifier1.Ccc, AddressOf Test
            AddHandler Me.notifier2.Ccc, AddressOf Test
    
            ' イベントを発生
            Me.notifier1.OnCcc("Xxx")
            Me.notifier2.OnCcc("Yyy")
            Me.notifier2.OnAaa()
            Console.WriteLine("イベント通知、完了!")
    
        End Sub
    
        ' WithEvents 付きのフィールドは、Handles の対象となる
        Private Sub Test(message As String) Handles notifier1.Ccc
            Console.WriteLine(message)
        End Sub
    
    End Class
    

  •  ここで注目すべき点が、Aaa イベントへのメソッド関連付けをしていないにも関わらず、イベント発行している点(Me.notifier2.OnAaa())です。 この場合、例外エラーは発生せず、関連付けられているメソッドが無いため何もせずに終わります。


イベントとは何か?(まとめ)

  •  イベント提供側(Button クラスとか)から見た場合、クラス定義した段階では、Event の準備はできますが、肝心のイベントが発生されたときの処理は準備できません。 なぜなら、【肝心のイベントが発生されたときの処理】は【このクラスを使う時になってから、やっと作るもの】だからです。

  •  そのため、イベント提供側は、Event (イベント用のデリゲート)を公開しておきます。 デリゲートの、メソッドをセットできる機能に頼るわけです。参照されていたら実行するし、参照されていなかったら何もしない。 この特徴を俯瞰的に見ると、イベントの処理内容が決まっておらず、入れ替えることができる。と捉えることができます。 ラムダ式や LINQ に通じる考え方ですね。
  • ' イベントを提供する側
    Class MyControl
    
        Public Click As Action(Of Object, EventArgs) = AddressOf ClickMethod
    
        Public Sub DoClick()
    
            If Me.Click IsNot Nothing Then
                Me.Click(Me, EventArgs.Empty)
            End If
    
        End Sub
    
        Private Sub ClickMethod(sender As Object, e As EventArgs)
            ' 具体的な処理は、このイベントを使う人に、作ってもらって任せよう
        End Sub
    
    End Class
    

  •  Event を使ったサンプルよりも Delegate を使ったサンプルの方が、【イメージしやすい】ので Delegate 版でサンプルを書いてみました。

  •  また、分かりやすさのため、文中で【メソッドをセットする】と書いていますが、変数に代入できる(メソッドそのものを入れることができる)と誤解を招く言い方になってしまいましたが、 正しくは、【メソッドを関連付ける】であり【メソッドの住所のみを参照する】ということになります。