デリゲートについて

デリゲートについておさらいしていきます。

目次

そもそもなんで存在しているのか、よくわからんの方

というか前提として、いまいちデリゲートのイメージがわかない・ピンとこない場合は、インターフェースとクラスの関係に当てはめてみると分かるかもしれません。"定義だけする側と実際の処理を作る側" の関係です。

Interface IButton
    Sub PerformClick()
End Interface

Class Button
    Implements IButton

    Public Click As Action = Nothing

    Public Sub PerformClick() Implements IButton.PerformClick

        If Click IsNot Nothing Then
            Click()
        End If

    End Sub

End Class

インターフェースのメソッド定義だけ版がデリゲートです。使いどころは同じではないですけども。

Delegate Sub ClickDelegate(s As String)

Module Module1

    Sub Click(s As String)
        Console.WriteLine(s)
    End Sub

    Sub Main()

        Dim method As ClickDelegate = AddressOf Click
        method("clicked!")

        ' または

        Dim method2 As Action(Of String) = AddressOf Click
        method2("clicked")


        Console.ReadKey()
    End Sub

End Module

メソッドの代行という機能

それではまず最初、デリゲートはメソッドを代行できる機能を持ちます。どういうことかというと以下のコードで見ていきます。

Module Module1

    Sub Print(message As String)
        Console.WriteLine(message)
    End Sub

    Function Plus(half As Integer, remain As Integer) As Integer
        Return half + remain
    End Function

    Delegate Sub PrintDelegate(s As String)
    Delegate Function PlusDelegate(i1 As Integer, i2 As Integer) As Integer

    Sub Main()

        ' 1.普通にメソッドを呼び出して実行する
        Print("Hello!")
        Dim result As Integer = Plus(1, 2)
        Console.WriteLine(result)

        ' 2.デリゲートが、メソッドの代わりに実行する
        Dim dummyPrint As PrintDelegate = New PrintDelegate(AddressOf Print)
        Dim dummyPlus As PlusDelegate = New PlusDelegate(AddressOf Plus)
        ' デリケートのインスタンス生成は省略可能
        'Dim dummyPrint2 As PrintDelegate = AddressOf Print
        'Dim dummyPlus2 As PlusDelegate = AddressOf Plus

        dummyPrint("Hello!")
        result = dummyPlus(1, 2)
        Console.WriteLine(result)


        Console.ReadKey()
    End Sub

End Module

出力結果

Hello!
3
Hello!
3

これは、まだジェネリックデリゲートが登場していない頃のコードです。ジェネリックデリゲートが使える昨今では、わざわざ自前でデリゲートを定義しないで、提供されているジェネリックデリゲートを使っていきましょう。ということで書き直したものが以下のコードです。

Module Module1

    Sub Print(message As String)
        Console.WriteLine(message)
    End Sub

    Function Plus(half As Integer, remain As Integer) As Integer
        Return half + remain
    End Function

    Sub Main()

        ' 1.普通にメソッドを呼び出して実行する
        Print("Hello!")
        Dim result As Integer = Plus(1, 2)
        Console.WriteLine(result)

        ' 2.デリゲートが、メソッドの代わりに実行する
        Dim dummyPrint As Action(Of String) = AddressOf Print
        Dim dummyPlus As Func(Of Integer, Integer, Integer) = AddressOf Plus

        dummyPrint("Hello!")
        result = dummyPlus(1, 2)
        Console.WriteLine(result)


        Console.ReadKey()
    End Sub

End Module

出力結果は同じです。インスタンス生成されたデリゲート(=ターゲットとなるメソッド(の住所)を参照指定した後)は、通常のメソッド呼び出しと同じ雰囲気で、デリゲートをメソッドのように実行させることができます。

イベント機構の陰の功労者

これが何の役に立つのかというと、イベント機構で役に立つことになります。Button の Click イベントとかのイベント機構です。次のコードを見てみます。

Dim dummyPrint As Action(Of String) = Nothing
If dummyPrint IsNot Nothing Then
    dummyPrint("Hello")
End If

これを実行するとどうなると思います?そうです、dummyPrint デリゲートは Nothing のままなので If 文には入らず、何もせずに終わります。この "もしも、メソッドをセットしているならば、処理を実行する(=つまり、メソッドをセットしていないならば何もしない)" という仕組みでイベントは成り立っています。次のコードを見てみます。

Class Button

    Public Click As Action = Nothing

    Public Sub PerformClick()

        If Click IsNot Nothing Then
            Click()
        End If

    End Sub

End Class

これは疑似ボタンクラスです。Click がイベントではなくデリゲートという違いがあります。とりあえず、使ってみましょう。使う側は以下のコードです。

Module Module1

    Sub Button_Click()
        Console.WriteLine("button clicked!")
    End Sub

    Sub Main()

        ' メソッドをセットしていない状態で疑似クリックしてみる
        Dim button1 As New Button
        button1.PerformClick()

        ' メソッドをセットしている状態で疑似クリックしてみる
        Dim button2 As New Button
        button2.Click = AddressOf Button_Click
        button2.PerformClick()


        Console.ReadKey()
    End Sub

End Module

出力結果は省略します。これはデバッグ実行しないと分かりづらいのですが、button1 ボタンではクリックしても何も起きずに終わるのに対して、button2 ボタンではクリックするとメッセージ出力の処理が実行されます。

この仕組みのおかげで、ボタンをクリックした際の実際の処理を、"この場で処理内容を決めて定義しなくてはいけない" ルールを飛び越えて、"後から処理内容をくっつける" というアプローチをとることができるようになります。作成タイミングをずらすことができるようになります。もちろん実行するときは、ちゃんと Nothing チェックしてから実行するので、NullReferenceException 対策も考慮されています。

デリゲートでイベント機構が実現できるのに、別途イベントキーワードが存在している理由は、主に以下の2点です。

  1. 公開しているデリゲートに対して、メソッドをセットできるということは、まだメソッドをセットしていないのに、公開しているデリゲートにアクセスすることで実行してしまう可能性がある
  2. WithEvents + Handles キーワード, または AddHandler, RemoveHandler キーワードによる動的なイベント処理の紐づけを使うことができるようになる

1.は以下のような使い方が(知ってる人ならやらないとしても)できてしまうことです。

Dim button3 As New Button
button3.Click()
' System.NullReferenceException: 'オブジェクト参照がオブジェクト インスタンスに設定されていません。'

2.はよく見るソースコードですね。デリゲートだけだとこれができません。

Class Button

    'Public Click As Action = Nothing
    Public Event Click As Action

    ' こういう風にメソッド形式でも書けるが、.NET Framework のソースコードでは、デリゲート指定する方の書き方をしている
    'Public Event Click()

    Public Sub PerformClick()
        RaiseEvent Click()
    End Sub

End Class

Module Module1

    WithEvents Button1 As Button = New Button

    Sub Button1_Click() Handles Button1.Click
        Console.WriteLine("button clicked!")
    End Sub

    Sub Button2_Click()
        Console.WriteLine("button clicked!")
    End Sub

    Sub Main()

        Dim button2 As New Button
        AddHandler button2.Click, AddressOf Button2_Click

        Button1.PerformClick()
        button2.PerformClick()


        Console.ReadKey()
    End Sub

End Module

イベント自体に関しては本題からそれてしまうのでこのくらいにしておきます。

LINQ の登場で大活躍

実感するのはメソッド構文の方ですが、ラムダ式と組み合わせたメソッドチェーンだと思います。

Module Module1

    Sub Main()

        Dim items = Enumerable.Range(1, 10).
            Where(Function(x) x Mod 2 = 0).
            Select(Function(x, i) New With {
                .Index = i,
                .Default = x,
                .Plus = x + x,
                .Multi = x * x}).
            OrderByDescending(Function(x) x.Index)

        items.
            ToList().
            ForEach(Sub(x) Console.WriteLine($"{x.Index} = {x.Default,2}, {x.Plus,2}, {x.Multi,3}"))


        Console.ReadKey()
    End Sub

End Module

出力結果

4 = 10, 20, 100
3 =  8, 16,  64
2 =  6, 12,  36
1 =  4,  8,  16
0 =  2,  4,   4

ここでは、Where, Select, OrderByDescending メソッドが対象となります。これらのメソッドの引数としてデリゲートが設定されていて、そこにラムダ式を渡して動かす仕組みになっています。

分かりやすくすると以下のような感じです。

Module Module1

    Sub Main()

        Dim items = New List(Of Integer) From {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        Dim results = Where(items, Function(i) i Mod 2 = 0)
        Dim message = String.Join(", ", results)
        Console.WriteLine(message)


        Console.ReadKey()
    End Sub

    ' 第一引数は、テスト対象のコレクション
    ' 第二引数は、チェック処理
    ' 戻り値は、チェックを通ったコレクション
    ' 
    ' デリゲートは第二引数、f : Func(Of Integer, Boolean)
    ' チェックする数字をもらって、Boolean を返す、戻り値ありのデリゲート
    Iterator Function Where(items As IEnumerable(Of Integer),
                            f As Func(Of Integer, Boolean)
                            ) As IEnumerable(Of Integer)

        For Each item As Integer In items

            If f(item) Then
                Yield item
            End If

        Next

    End Function

End Module

出力結果

2, 4, 6, 8, 10

ポイントは、メソッド内でデリゲートを実行してその結果をもとに、場合分け処理を行っている点にあります。メソッドの処理内容が固定ではなく可変になります。上記の処理内容は偶数のみ抽出するような処理内容ですが、Function(i) i Mod 2 <> 0 と変えるだけで奇数のみ抽出するようにメソッドの機能を変えることが可能になります。その場で作って、その場で使って終わり。みたいな使い捨てメソッドを用意することが可能になります。

イテレータとイールドが謎の場合は、普通のメソッドというかコレクションの戻り値に変えて考えても同じです。やはり "処理内容を外から指定できる" ことが重要な点になります。

また、こういう使い方はしないと思いますが、こういう風にいちいちメソッドを用意していたら保守が大変になります。一時的メソッドはラムダ式で代替、デリゲートは組み込みで代替、となることで、一連の処理を続けて実行することができます。条件分岐が消えるので見通しが良くなります。

Function IsEven(i As Integer) As Boolean
    Return i Mod 2 = 0
End Function

Function IsOdd(i As Integer) As Boolean
    Return i Mod 2 <> 0
End Function

Sub Main()

    Dim items = New List(Of Integer) From {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    Dim results = Where(items, AddressOf IsEven)
    Dim message = String.Join(", ", results)
    Console.WriteLine(message)


    Console.ReadKey()
End Sub

Where メソッドは省略

非同期実行ができる

のですが、非同期処理については、Async/Await + Task の方にお任せした方が良いと思います。

最後に

イベントにしろ LINQ にしろ、デリゲートは xxx と組み合わせて使うものです。そして、相手を立てて自分は縁の下の力持ち的な役回りを担当するので、結局日の光を浴びることが少なく、主役にならないので、よくわからん、になってしまいます。でも大事な役目を担当しているので、たまには思い出してね。