デリゲートについて
デリゲートについておさらいしていきます。
目次
そもそもなんで存在しているのか、よくわからんの方
というか前提として、いまいちデリゲートのイメージがわかない・ピンとこない場合は、インターフェースとクラスの関係に当てはめてみると分かるかもしれません。"定義だけする側と実際の処理を作る側" の関係です。
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点です。
- 公開しているデリゲートに対して、メソッドをセットできるということは、まだメソッドをセットしていないのに、公開しているデリゲートにアクセスすることで実行してしまう可能性がある
- 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 と組み合わせて使うものです。そして、相手を立てて自分は縁の下の力持ち的な役回りを担当するので、結局日の光を浴びることが少なく、主役にならないので、よくわからん、になってしまいます。でも大事な役目を担当しているので、たまには思い出してね。