VB のたまご

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


ラムダ式とデリゲート(2017年10月版)

  •  ラムダ式とデリゲートの関係について、簡単に解説しましたので共有します。


ラムダ式とは何か

  •  ラムダ式とは、あらかじめメソッドを定義しなくても使える、その場で作るメソッドのことです。
  • ' ラムダ式って何?
    Public Sub Test1()
    
        ' ラムダ式は、あらかじめ定義していない一時的な即席メソッドの事(だからメソッド名が無い)
        ' 定義していないから、このメソッド内でしか使えない
        Dim showMessage2 = Sub(s As String) Console.WriteLine(s)
    
        ' 使う時は普段通りのメソッド呼び出しと同じ
        ShowMessage1("hello")
        showMessage2("hello")
    
        ' hello
        ' hello
    
    End Sub
    
    Private Sub ShowMessage1(s As String)
        Console.WriteLine(s)
    End Sub
    

  •  と言っても、定義してしまえば後は従来のメソッドと使い方は同じですので、特別気にする点は無いかと思います。 あらかじめ定義して使うメソッドに、戻り値があるメソッド、無いメソッドがあるように、ラムダ式でも戻り値があるメソッド、無いメソッドの書き方がありますので、見てみます。

  •  まずは、戻り値が無いラムダ式です。
  • ' Sub なラムダ式
    Public Sub Test2()
    
        ' 単一行での書き方
    
        ' Sub() 処理内容、または、
        ' Sub(引数1、引数2、... ) 処理内容、という記載ルール
    
        ' メソッド名は無いが、呼び出して使えるようにするため、変数にセットして使う
        ' この変数がメソッド名の代わりにもなる
        Dim f1 = Sub() Console.WriteLine("aaa")
        Dim f2 = Sub(s As String) Console.WriteLine(s)
    
        f1()
        f2("hello")
    
        ' aaa
        ' hello
    
        ' ある意味「メソッド」という戻り値を返している
        ' ラムダ式には型が無い(マウスオーバーすると以下のような型を教えてくれるが、明示的には型宣言できない)
        'Dim f3 As <Sub()>= Sub() Console.WriteLine("aaa")
    
        ' 型が無いので、引数としてメソッドに渡すこともできないし、ここで作ったラムダ式は、このメソッド内でしか使えない
    
        ' 処理を省略することはできない
        'Dim f4 = Sub()
    
    
    
        ' 複数行での書き方
        ' sub() まで書いて Enter キーなどで改行したタイミングで、End Sub が自動挿入される
        ' 複数行形式の場合、Sub と End Sub が必要になる(メソッドの範囲が分かる)
        ' 処理を省略することができる(が、何の意味もない)
        Dim f5 = Sub()
    
                 End Sub
    
        Dim f6 = Sub(i1 As Integer, i2 As Integer)
                     Dim i3 = i1 + i2
                     Console.WriteLine(i3)
                 End Sub
    
        f5()
        f6(1, 2)
    
        ' 3
    
    End Sub
    

  •  ラムダ式では、単一行形式の書き方と複数行形式の書き方の2パターンがあります。 単一行形式の場合は、End Sub と記載しない、シンプルで簡潔な書き方になっていて、Sub() の後ろを処理内容と判断します。

  •  ラムダ式はメソッドなので、定義したままだと扱う(呼び出す)ことができません。 そのため、変数にメソッドをセットして扱います。 複数行形式の場合は、メソッドの範囲が分かるように、End Sub を付けます。

  •  続いては、戻り値があるラムダ式です。
  • ' Function なラムダ式
    Public Sub Test3()
    
        ' 単一行での書き方
    
        ' Function() 処理内容、または、
        ' Function(引数1、引数2、... ) 処理内容、という記載ルール
        ' 戻り値の型は書かない(戻り値から推論される)
        ' Return は書かない(処理内容から推論される)
        ' 変数にマウスオーバーすると型が分かる
        Dim f1 = Function() 1
        Dim f2 = Function(i1 As Integer, i2 As Integer) CStr(i1 + i2) & "でした"
    
        Dim ret1 = f1()
        Dim ret2 = f2(1, 2)
        Console.WriteLine(ret1)
        Console.WriteLine(ret2)
    
        ' 1
        ' 3でした
    
    
    
        ' 複数行での書き方
        ' function() まで書いて Enter キーなどで改行したタイミングで、End Function が自動挿入される
        ' 複数行形式の場合、Function と End Function が必要になる(メソッドの範囲が分かる)
        ' 戻り値の型は省略できる
        ' Return は必ず書く
        Dim f3 = Function()
                     Return 3
                 End Function
    
        ' 戻り値の型は記載できる
        Dim f4 = Function() As Integer
                     Return 3
                 End Function
    
        Dim ret3 = f3()
        Dim ret4 = f4()
        Console.WriteLine(ret3)
        Console.WriteLine(ret4)
    
        ' 3
        ' 3
    
    End Sub
    

  •  Function ラムダ式の場合も Sub ラムダ式と同様です。


デリゲートが出てくるのはなぜか、デリゲートを宣言しないで渡しても大丈夫なのはなぜか

  •  ラムダ式を扱う時、必ずデリゲートが一緒に出てきます。 なぜデリゲートが出てくるのでしょうか。その答えを知るためには、先にデリゲートの使い方を知っている必要があります。 まずは、デリゲートの使い方を覚えましょう。ここで言うデリゲートは、ジェネリックデリゲートのことを指しています。

  •  デリゲートは、メソッドと対になって初めて仕事ができる型です。 例えば、あらかじめ定義済みのメソッドと組んだり、ラムダ式と組んだりできます。

  •  デリゲートがいまいちよくイメージできない場合は、以下のように考えると分かりやすいかもしれません。 デリゲートは処理内容が無いメソッドみたいなもので、そこに実際に定義したメソッドを関連付けて使います(デリゲートが処理を呼び出せる)。 これに近いものとして、インターフェース(のメソッド)とインターフェースを実装したクラス(のメソッド)の関係に似ています。

  •  インターフェース(のメソッド)はそれ自体には処理内容が含まれておらず、実装したクラス(のメソッド)側に実際に定義した処理内容が含まれます。 使う際は、インターフェース型の変数に、実装したクラスのインスタンス生成したオブジェクトをセットして使うことができますが、このような関係に似ています。 中身が無いものが、中身があるものを使う。という点だけで、仕組みは全然違いますが。。。
  • ' デリゲート
    Public Sub Test4()
    
        ' デリゲートは、メソッドを参照する型(詳しくは割愛)
        ' 戻り値が無いメソッド用の Action デリゲート、戻り値があるメソッド用の Func デリゲートがある
        ' 宣言方法は後述。以下では、使用事例を見てみる
    
    
    
        ' 定義済みメソッドを参照して使う
        Dim d1 = New Action(AddressOf Hello)
        Dim d2 = New Func(Of Integer, Integer, Integer)(AddressOf Plus)
    
        d1()
        Dim ret1 = d2(1, 2)
        Console.WriteLine(ret1)
    
        ' hello
        ' 3
    
    
    
        ' 定義済みメソッド以外にも、ラムダ式も扱える
        Dim d3 = New Action(Sub() Console.WriteLine("hello"))
    
        ' デリゲート越しだと、型推論が効く(引数 i1, i2 の部分)
        Dim d4 = New Func(Of Integer, Integer, Integer)(Function(i1, i2) i1 + i2)
    
        d3()
        Dim ret2 = d4(1, 2)
        Console.WriteLine(ret2)
    
        ' hello
        ' 3
    
    
    
        ' 型を明示的に書いていると、デリゲートのインスタンス生成を省略できる
        Dim d5 As Action = New Action(AddressOf Hello)
        Dim d6 As Action = AddressOf Hello
    
        ' デリゲートに限っては、型と関連付けるメソッドは、分けた方が把握しやすい
        Dim d7 As Action = Sub() Console.WriteLine("hello")
        Dim d8 As Func(Of Integer, Integer, Integer) = Function(i1, i2) i1 + i2
    
    End Sub
    
    Private Sub Hello()
        Console.WriteLine("hello")
    End Sub
    
    Private Function Plus(i1 As Integer, i2 As Integer) As Integer
        Return i1 + i2
    End Function
    

  •  デリゲートにも、戻り値が無いメソッド用のデリゲート、戻り値があるメソッド用のデリゲートに分かれています。

  •  まずは、戻り値が無いメソッド用のデリゲートです。
  • ' Action デリゲート
    Public Sub Test5()
    
        ' 戻り値を型推論に頼る場合は、デリゲートのインスタンス生成と関連付けるメソッドを書く
        ' この時、デリゲート側の引数と、メソッド側の引数が一致していること
        Dim d1 = New Action(Sub() Console.WriteLine("aaa"))
        Dim d2 = New Action(Of String)(Sub(s As String) Console.WriteLine(s))
        Dim d3 = New Action(Of String)(Sub(s) Console.WriteLine(s))
    
        ' 戻り値を明示的に書く場合は、デリゲートのインスタンス生成は省略可能
        Dim d4 As Action = New Action(Sub() Console.WriteLine("bbb"))
        Dim d5 As Action = Sub() Console.WriteLine("ccc")
    
        Dim d6 = New Action(Of Integer, Integer)(Sub(i1, i2)
                                                     Console.WriteLine(i1 + i2)
                                                 End Sub)
    
        Dim d7 As Action(Of Integer, Integer) = Sub(i1, i2)
                                                    Console.WriteLine(i1 + i2)
                                                End Sub
    
        ' 左側の空欄が気になる、または右に行き過ぎて見づらい場合は、適度な改行を入れる
        Dim d8 As Action(Of Integer, Integer) =
            Sub(i1, i2)
                Console.WriteLine(i1 + i2)
            End Sub
    
        ' = ではなく、As でも可能
        Dim d9 As New Action(Of String)(Sub(s) Console.WriteLine(s))
    
    
    End Sub
    

  •  戻り値が無いデリゲートは、Action デリゲートが担当します。 デリゲートはメソッドの代役なので、組み合わせるメソッドの、引数や戻り値を合わせる必要があります。

  •  続いては、戻り値のあるデリゲートです。
  • ' Func デリゲート
    Public Sub Test6()
    
        ' 基本は Action デリゲートと同じ
        ' 最後の型指定は、戻り値用の型を指定する事に注意
        Dim d1 = New Func(Of Integer)(Function() 2) ' これは引数の Integer ではなく、戻り値の Integer
        Dim d2 = New Func(Of Integer, Integer)(Function(i) i + 2)
        Dim d3 = New Func(Of Integer, Integer, Integer)(Function(i1, i2) i1 + i2)
    
        Dim d4 As Func(Of Integer) = Function() 2
        Dim d5 As Func(Of Integer, Integer) = Function(i) i + 2
        Dim d6 As Func(Of Integer, Integer, Integer) = Function(i1, i2) i1 + i2
    
        ' d1()    : 2
        ' d2(1)   : 3
        ' d3(2, 3): 5
    
        ' d4()    : 2
        ' d5(1)   : 3
        ' d6(2, 3): 5
    
        Dim d7 As Func(Of Integer, Integer, Integer) =
            Function(i1, i2)
                Return i1 + i2
            End Function
    
    
    
        ' デリゲートにデリゲートを渡すこともできる
        ' メソッド内で、任意の判定処理に該当するデータのみにフィルタするメソッド
        Dim getData As Func(Of Integer, Func(Of Integer, Boolean), Integer) =
            Function(i, predicate)
    
                If predicate(i) Then
                    Return i
                Else
                    Return 0
                End If
    
            End Function
    
        ' 外部から判定処理を受け入れることで、getData メソッドが扱える範囲が広がる
        ' 偶数のみ取得するメソッドとして使う(Xxx 判定に適合したデータを返却するメソッド)
        Dim ret1 = getData(1, Function(x) x Mod 2 = 0)
        Dim ret2 = getData(2, Function(x) x Mod 2 = 0)
    
        ' 0
        ' 2
    
        ' = ではなく As でも可能
        Dim d8 As New Func(Of Integer)(Function() 2)
    
    
    End Sub
    

  •  戻り値があるデリゲートは、Func デリゲートが担当します。 戻り値があるので、最低1つの型指定が必要になります。 ここで誤解しやすいポイントがあり、引数の型指定と戻り値の型指定が、混ざってしまい混乱してしまう事です。 最後に書くのは戻り値用、それ以外は引数用、と覚えてください。


ラムダ式は、何に使うのか

  •  ほぼ LINQ (のうちメソッド構文)用途で使うことになります。

  •  例えば以下です。
  • ' LINQ のメソッド構文
    Public Sub Test7()
    
        ' LINQ のメソッド構文として使われている
    
        ' 1-10 までの数字のうち、偶数だけに絞り込み、偶数を二乗したコレクションを返す
        Dim items = Enumerable.Range(1, 10).
            Where(Function(x) x Mod 2 = 0).
            Select(Function(x) x * x)
    
        items.
            ToList().
            ForEach(Sub(x) Console.WriteLine(x))
    
        ' 1行で書けるくらいの簡単な判定でよく使う
        ' このように、ラムダ式は、一時的な使い捨てメソッドとして使える
    
        ' 一見、デリゲートが出てきておらず、直接ラムダ式を渡しているように見えるが、
        ' デリゲートがインスタンス生成を省略できる特徴があるため、わざわざ書いていないだけ
    
        ' 例えば、Where 拡張メソッドを見ると、
        ' 引数で受け取るのは、predicate As Func(Of TSource, Boolean) とデリゲート指定になっている
        ' ※ TSource は任意の型。今回は x:Integer 型として使っている
        '    Boolean は戻り値の型。x Mod 2 = 0 という処理結果からの推論を当てはめている
        ' 
        ' Public NotInheritable Class Enumerable
        ' 
        '     <Extension>
        '     Public Shared Function Where(Of TSource)(source As IEnumerable(Of TSource), 
        '                                              predicate As Func(Of TSource, Boolean)) As IEnumerable(Of TSource)
        ' 
        ' End Class
    
    End Sub
    

  •  LINQ をパッと見た感じ、SQL ライクな書き方により、すっきりしていて処理を把握しやすいのは、クエリ構文の方です。
  • ' LINQ のクエリ構文とメソッド構文(+ラムダ式)
    Public Sub Test8()
    
        Dim items1 = From x In Enumerable.Range(0, 10)
                     Where x Mod 2 = 0
                     Select x * x
    
        Dim items2 = Enumerable.Range(1, 10).
            Where(Function(x) x Mod 2 = 0).
            Select(Function(x) x * x)
    
    End Sub
    

  •  それなのに、巷で見かける記事に多いのはメソッド構文の方です。 この利用としては、メソッド構文の方がより柔軟な命令がそろっているからなのですが、この記事での本題とは別の話になってしまうため、ここでは省略します。

  •  最後に、デリゲートに関係する他の技術として、イベント機構があります。 これも大元はデリゲートなので、ラムダ式を適用することが可能です。
  • ' イベントのデリゲート
    Public Sub Test9()
    
        ' イベントの実体はデリゲートなので、引数が一致するラムダ式を関連付けることができる
        ' デリゲートへの関連付けなので、ラムダ式の引数の型は推論してくれる
        Dim button1 = New Button
        AddHandler button1.Click, Sub(sender, e) Console.WriteLine("aaa")
    
        ' または、
        AddHandler button1.Click, Sub(sender, e)
                                      Console.WriteLine("bbb")
                                  End Sub
    
    
    End Sub
    


まとめ

  •  ここでは、ラムダ式とデリゲートの関係性について見てきました。ラムダ式は便利な機能ですが型を持っていないため、メソッドの引数としてラムダ式を渡すことができません。 これに対応するためにデリゲートを間に挟んで使います。デリゲートはデリゲートとしての型を持っているので、引数や戻り値の定義が一致していれば、ラムダ式をデリゲートに渡すことができます。

  • デリゲートとラムダ式のポイント
    
    ■デリゲート
    単体では何もできない
    Action ジェネリックデリゲート、引数無し、あり
    Func ジェネリックデリゲート、引数無し、あり
    定義済みメソッドかラムダ式、要するにメソッドを参照して扱う
    →定義済みメソッドの参照が分かりやすいが、メソッドをセットしているように見えるが、
     AddressOf メソッド名という記載ルールになっている
     Address(住所)Of(~の)メソッド名。という英語から、メソッドの住所をセットしている
    
    ■ラムダ式
    メソッド内でしか使えない無名メソッド(クラスメンバーではないメソッド)
    Sub ラムダ式、引数無し、あり
    Function ラムダ式、引数無し、あり
    1行で書けるくらいの、簡単な判定でよく使う
     → この程度のメソッドをあらかじめ定義しておくと大量の管理になる
     → 一時的にしか使わない、何回も使わない、メンテナンスしなくてもいい使い捨ての一時メソッドみたいな扱い
       → LINQ のメソッド構文