Visual Basic 初級講座 [改訂版]
VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

第35回 ラムダ式の背後にあるもの

2021/5/30

この記事が対象とする製品・バージョン

VB2019 Visual Basic 2019 対象です。
VB2017 Visual Basic 2017 対象です。
VB2015 Visual Basic 2015 対象です。
VB2013 Visual Basic 2013 対象です。
VB2012 Visual Basic 2012 対象です。
VB2010 Visual Basic 2010 対象です。
VB2008 Visual Basic 2008 対象ですが、VB2008では使えない機能も扱っています。
VB2005 Visual Basic 2005 × 対象外です。ラムダ式はVB2008から導入された機能です。
VB.NET 2003 Visual Basic.NET 2003 × 対象外です。
VB.NET 2002 Visual Basic.NET (2002) × 対象外です。
VB6対応 Visual Basic 6.0 × 対象外です。

 

目次

 

1.クロージャ

1-1.外部の変数をラムダ式内で使う

ラムダ式の外側で定義されている変数をラムダ式の中で使用することができます。

次のプログラムは、ラムダ式の外側で定義されている変数xとyを、ラムダ式の中で使っています。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim x As Integer = 2
Dim y As Integer = 3
Dim add = Function() x + y

変数とラムダ式のスコープが違う場合でも動作します。

次のようなケースです。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Test()
    Dim add = GenerateAdd()
End Sub

Private Function GenerateAdd() As Func(Of Integer)
    Dim x As Integer = 2
    Dim y As Integer = 3
    Return Function() x + y
End Function

この例では、GenerateAddメソッドはラムダ式を返すメソッドです。が、そのラムダ式の中にはローカル変数の x と y が使用されています。GenerateAdd メソッドの実行が終了した時点で、x と y はスコープが外れるため破棄されますが、それを使っているラムダ式は戻り値として帰っていくので、生き残ります。

これでも、ラムダ式は期待通りに動いてくれます。

VBは内部で、自動的にクラスを生成して、変数x, y をそのクラスの中に保存します。このようなクラスを「クロージャ」と呼び、変数がそこに格納されることを「変数のキャプチャー」と呼びます。ラムダ式はクロージャに保存されている変数を見に行くので、ローカル変数 x と y がスコープからはずれても、期待通り動作するわけです。なお、ラムダ式本体もクロージャの中に保存されます。

クロージャはVBがコンパイル時に自動生成しているので、プログラムからは通常見たりアクセスすることはできませんが、このようなものが存在するということを覚えておく必要はあります。

数値型のような単純なものならキャプチャーされてもあまり問題になりませんが、終了処理が必要なオブジェクトがキャプチャーされた場合、期待したタイミングで終了処理が実行できないかもしれません。たとえば、ラムダ式の外側にあるデータベースへの更新や、ファイルへの書き込みを行うオブジェクトをラムダ式がキャプチャーした場合、更新の完了や書き込みの終了のタイミングの制御が難しくなります。(難しいだけではなく、キャプチャーされていることに気が付かないでバグの原因になってしまうかもしれません。)

 

1-2.キャプチャーされた変数の実験

パラメーターをキャプチャーさせる実験

VBは、内部で起こっていることをできるだけプログラマーの目から隠して、プログラマーが期待している通りにプログラムが動くように見せかけます。

少し実験してみましょう。 次の関数は引数で与えられた数字(number)を2乗するラムダ式を返します。。。

※2乗とは、2回かけ算するという意味で、3の2乗は9, 4の2乗は16, 10の2乗は100という具合です。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Function GeneratePower(number As Integer) As Func(Of Numerics.BigInteger)

    Dim MyFunc = Function()
                     Dim bigNumber As Numerics.BigInteger = number
                     Return bigNumber * bigNumber
                 End Function

    Return myFunc

End Function

2乗するとかなり巨大な数になる場合があるので、戻り値は Integer ではなく、Numerics.BigInteger としました。BigInteger は桁数無制限の整数を扱える型です。

このプログラムのポイントは、ラムダ式の内部で使用している number という変数が、ラムダ式の外部で定義されている変数(パラメーター)であるという点です。この number パラメーターはクロージャにキャプチャーされます。

 

これを実際に呼び出す例は次のようになります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Test()

    Dim x As Integer = 12
    Dim power = GeneratePower(x)
    Dim result = power() '12 × 12 で 144 になります。

    Debug.WriteLine(result)

End Sub

Private Function GeneratePower(number As Integer) As Func(Of Numerics.BigInteger)

    Dim MyFunc = Function()
                     Dim bigNumber As Numerics.BigInteger = number
                     Return bigNumber * bigNumber
                 End Function

    Return myFunc

End Function

この例では 引数に 12 を指定しているので、結果として 12 × 12 の 144 が表示されます。

 

キャプチャー後に呼び出し元で値を変更

実験はここからです。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Test()

    Dim x As Integer = 12
    Dim power = GeneratePower(x)
    x = 4

    Dim result = power() '12 × 12 で 144 になります。

    Debug.WriteLine(result)

End Sub

Private Function GeneratePower(number As Integer) As Func(Of Numerics.BigInteger)

    Dim MyFunc = Function()
               Dim bigNumber As Numerics.BigInteger = number
               Return bigNumber * bigNumber
           End Function

    Return myFunc

End Function

ラムダ式生成後に x = 4 というように x の値を書き換えるとどうなるでしょうか?

結果は変わらず 144 です。

ラムダ式にキャプチャーされているのは、変数x ではなく、パラーメーター number なのです。 x の値が変わっても影響しません。

 

ByRefのパラメーターはラムダ式内で使用できない

それでは、引数の定義に ByRef を付けて ByRef number As Integer とするとどうなるでしょうか。これだと、 x と number は同じモノということになるのですが、今度は VB がエラーにします。「エラー BC36639 'ByRef' パラメーター 'number' をラムダ式で使用することはできません。 」という出力されます。

この状態だと複雑すぎるので、変数のキャプチャーを実行しないで、エラーにされてしまうというわけです。

 

キャプチャー後にキャプチャーしたパラメーターの値を変更

それでは、今度は次のようにしてみましょう。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Test()

    Dim x As Integer = 12
    Dim power = GeneratePower(x)

    Dim result = power() '5 × 5 で 25 になります。

    Debug.WriteLine(result)

End Sub

Private Function GeneratePower(number As Integer) As Func(Of Numerics.BigInteger)

    Dim MyFunc = Function()
               Dim bigNumber As Numerics.BigInteger = number
               Return bigNumber * bigNumber
           End Function

    number = 5

    Return myFunc

End Function

ラムダ式生成後にパラメーター number の値を書き換えてしまいます。

結果は 25 と表示されます。

ラムダ式生成後にキャプチャーされた変数を変更すると、ラムダ式はちゃんと変更された値を見てくれるということです。

 

一連の実験で、多くの人は、何やら複雑で面倒だなと感じられたのではないかと思います。

外部の変数を使用するラムダ式は使わないに越したことはありませんし、使う場合でも変数と同じスコープするなど、わかりやすくなるように配慮するのが良いでしょう。

 

 

2.デリゲート

2-1.デリゲートの考え方

デリケートはメソッドを表現する型です。たとえば、引数が2つあって、1つ目はStringで2つ目はIntegerで戻り値は Double であるというような定義をするための型です。ですから、ラムダ式を表現するのにぴったりというわけです。Delegateキーワード(読み方:Delegate=デリゲート)を使うと自分でデリゲートを定義することもできます。

1つ目の引数がString、2つ目の引数がIntegerで戻り値がDouble であるメソッドは次のようなデリゲート型で表現できます。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Public Delegate Function SampleDelegate(p1 As String, p2 As Integer) As Double

デリゲートは型なので、この型の変数を使って利用します。その変数には、このデリゲートと定義が一致するメソッドを代入して、呼び出すことができます。

デリゲートにメソッドを代入するときには、AddressOf演算子(読み方:AddressOf=アドレスオブ)を使うのが特徴的です。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Delegate Function SampleDelegate(p1 As String, p2 As Integer) As Double

Private Function SampleFunction(arg1 As String, arg2 As Integer) As Double
    Return 123
End Function

Private Sub Test()

    Dim sd As SampleDelegate
    sd = AddressOf SampleFunction

    'SampleFunctionが呼び出され、result の値は 123 になります。
    Dim result As Double = sd("ABC", 222)

End Sub

 

デリゲートはVB.NET登場当初からある技術で、ラムダ式より前から存在しました。

ラムダ式を使うと同じことをもっと簡単に実現できるので、VB2008以降はデリゲートの出番はほとんどありません。

たとえば、上述の例はラムダ式を使って次のように書き換えることができます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Function SampleFunction(arg1 As String, arg2 As Integer) As Double
    Return 123
End Function

Private Sub Test()

    Dim sd = Function(p1 As String, p2 As Integer) SampleFunction(p1, p2)

    'SampleFunctionが呼び出され、result の値は 123 になります。
    Dim result As Double = sd("ABC", 222)

End Sub

これだけでも小難しい Delegate や AddressOf というキーワードがなくなり、行数も減って、だいぶシンプルに感じます。状況によってはさらに次のように簡略化することもできます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Test()

    Dim sd = Function(p1 As String, p2 As Integer) 123

    'ラムダ式が実行され、result の値は 123 になります。
    Dim result As Double = sd("ABC", 222)

End Sub

 

というわけなので、2021年現在では、デリゲートについては、そういうものもあるという程度のことを知っていればよく、ラムダ式の方を活用するのが良いでしょう。

 

2-2.ラムダ式との互換性

デリゲートにはラムダ式との互換性もあり、同じ定義のラムダ式を代入することもできます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Delegate Function SampleDelegate(p1 As String, p2 As Integer) As Double

Private Sub Test()

    Dim sd As SampleDelegate = Function(p1 As String, p2 As Integer) 123

    'SampleFunctionが呼び出され、result の値は 123 になります。
    Dim result As Double = sd("ABC", 222)

End Sub

この例もありがたみは特になく、ラムダ式に型を明示したければActionやFuncを使ってもっと簡単に書けることは前回説明しています。次のようになります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Test()

    Dim sd As Func(Of String, Integer, Double) = Function(p1 As String, p2 As Integer) 123

    'SampleFunctionが呼び出され、result の値は 123 になります。
    Dim result As Double = sd("ABC", 222)

End Sub

 

ラムダ式を代入できるものには、Func や Action 、前回紹介した Predicate 、それにイベント があります。これらはすべて、デリゲートでもあります。

Func や Action の定義を Microsoft Docs で確認すると、Delegate として定義されていることが確認できます。つまり上述の例で、Public Delegate Function SampleDelegate と定義したのと同じものを、フレームワークがあらかじめ定義してすぐ使えるようにしてくれているだけです。もっとも、いろいろな型に対応できるようにジェネリックにして、さらに引数の数に応じたたくさんの定義を用意してくれているという点で配慮は行き届いています。

Func<TResult> 代理人 (System) | Microsoft Docs

このドキュメントでは Delegate が日本語に翻訳されて「代理人」となっています。一般的にはここは日本語に翻訳せずにカタカナで「デリゲート」と表現します。

前回の説明で、「ラムダ式はいろいろなものに代入できるみたいだけど、いったいどういう法則やルールがあるんだろう?」と疑問に感じた方もいるかもしれません。これが答えです。ラムダ式はデリゲート型に代入されていたというわけです。

次のプログラムではラムダ式の型をVBに推論させて、どのような型になったか確認できます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim s1 = Sub() Debug.WriteLine("TEST")
Debug.WriteLine("型の名前:" & s1.GetType.FullName)

If TypeOf s1 Is [Delegate] Then
    Debug.WriteLine("s1 は デリゲートです。") 'これが表示されます。
End If

Debug.WriteLineで出力される場所

実行すると 次のように出力されます。

型の名前:VB$AnonymousDelegate_0
s1 は デリゲートです。

 Action でも Func でも、Predicate でもなく、VB$AnonymousDelegate_0 という名前の独自の型になっていることがわかります。さらに Delegate であるかどうか確認する If 文にも合格し、 s1 は デリゲートであるとはっきり出力されます。

型名は固定ではないのでVB$AnonymousDelegate_0という名前は場合によっては違う名前になっているかもしれません。これはVBがコンパイル時に自動生成したデリゲート型です。名前にVBで使用できない文字「$」が含まれているのはプログラマーが定義した他の名前と重複しないようにするためです。

この自動生成される型はコンパイル時に作られるのであって、プログラム時点では存在していません。だから、次のように型を明示するプログラムは意味がなく、エラーになります。

'この例はエラーです。
Dim s1 As VB$AnonymousDelegate_0 = Sub() Debug.WriteLine("TEST")
Debug.WriteLine(s1.GetType.FullName)

そもそも、型の名前に使えない記号 $ がついているので、構文上も無理があります。

 

3.ラムダ式の代入と呼び出し

3-1.( ) の有無による違い

ラムダ式を使用しているときは、普段あまり意識していない ( ) に注意が必要です。

次の例はラムダ式を実行し、変数 result の値は 627 になります。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'固定で627を返すラムダ式
Dim lambda = Function() 627

'result は 627 になります。
Dim result = lambda()

最後の行で lambda() と ( ) がついていることに注意してください。

この ( ) があるから、ラムダ式で定義している処理が実行され戻り値が reuslt に代入されるのです。

一方、次のプログラムは lambda の後ろに ( ) がついていません。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'固定で627を返すラムダ式
Dim lambda = Function() 627

'result は 627 を返すラムダ式になります。
Dim result = lambda     ' ← ( ) がついていないのでラムダ式は実行されません。

'value は 627 になります。
Dim value = result()

結果として、result はラムダ式自身を表すことになり、result( ) と記述することでラムダ式の処理を実行できるようになります。もちろん、lambda( ) と書いても同じです。

このように ( ) の有無でまるで意味が変わります。

 

3-2.コレクションを返すラムダ式

ラムダ式の戻り値が配列やコレクションの場合は、もっと紛らわしいです。配列やコレクションの要素を取得するときにも ( ) を使うので、これとの区別がちょっとだけややこしいからです。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'4つの要素からなる文字列型の配列を返すラムダ式
Dim lambda = Function() {"Apple", "Banana", "Cat", "Dog"}

'result1 は ラムダ式になります。
Dim result1 = lambda

'result2 は 4つの要素からなる文字列型の配列になります。
Dim result2 = lambda()

'result3 は "Apple" になります。
Dim result3 = lambda()(0)

'エラーです。このラムダ式には引数がないのに引数を指定しているからです。
'Dim result4 = lambda(0)

こうやって並べて書くと 最後の result4 がエラーになるのはそれほど難しくないのですが、慣れないうちは、このような書き方をして原因がわからずうなってしまうこともあるかもしれません。

 

さらに言うと、Dim lambda = Function() Function() 5 のように、ラムダ式を返すラムダ式や、Dim lambda = {Function() 1, Function() 2, Function() 3} のように、ラムダ式の配列も書けるので、( ) の使い方を複雑にしようと思えば、いろいろと細工はできます。

今のところ、このようなわかりにくいラムダ式の使い方を私は見たことはありません。

 

4.ラムダ式の活用

4-1.非同期実行

ラムダ式はこれまでに説明した以外にもいろいろな活用方法があります。初級講座では深入りしないつもりの部分もありますが、簡単に紹介します。

ラムダ式を使うと簡単に非同期実行・マルチスレッドのプログラムを実現することができます。

次の例はWindowsフォームアプリケーションで実行すると MsgBox を2つ同時に表示します。通常MsgBoxは閉じられるまで次のプログラムが実行されないので2つ同時に表示することはできませんが、マルチスレッドであれば実現できます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Task.Factory.StartNew(Sub() MsgBox("Hello!"))
Task.Factory.StartNew(Sub() MsgBox("Himiko also known as Shingi Wao was a shamaness-queen of Yamatai-koku in Wakoku"))

 

4-2.LINQ

LINQのクエリ式を使って、集合を操作することができます。条件を指定した抽出や、並び替えや集計などさまざまなことができます。LINQについては今後初級講座で取り上げる予定です。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names = New String() {"徳川家康", "織田信長", "徳川吉宗", "豊臣秀吉", "徳川慶喜"}

'「徳川」から始まるものの抽出を定義 = LINQのクエリ式
Dim tokugawas = From name In names Where name.StartsWith("徳川")

'抽出を実行し、結果を表示
tokugawas.ToList.ForEach(Sub(name) Debug.WriteLine(name))

この例のLINQのクエリ式は下記のプログラムの書き換えです。形を変えたラムダ式というわけです。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim names = New String() {"徳川家康", "織田信長", "徳川吉宗", "豊臣秀吉", "徳川慶喜"}

'「徳川」から始まるものの抽出を定義 = LINQのクエリ式
Dim tokugawas = names.Where(Function(name) name.StartsWith("徳川"))

'抽出を実行し、結果を表示
tokugawas.ToList.ForEach(Sub(name) Debug.WriteLine(name))

 

4-3.式ツリー

一行形式のラムダ式は簡単に式ツリーに変換することができます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'▼この1行だけでラムダ式から式ツリーを取得できます。
Dim tree As Expressions.Expression(Of Func(Of Integer, Integer)) = Function(value) value + 6

'▼式ツリーのごく簡単な解析。
'いろいろな処理があるので決め打ちで簡単に雰囲気を紹介します。
Dim ex = DirectCast(tree.Body, Expressions.BinaryExpression)

If ex.NodeType = Expressions.ExpressionType.AddChecked Then
    Debug.WriteLine("たし算です")
End If

Dim left = DirectCast(ex.Left, Expressions.ParameterExpression)
Debug.WriteLine("左辺は " & left.Name)

Dim right = DirectCast(ex.Right, Expressions.ConstantExpression)
Debug.WriteLine("右辺は " & right.Value.ToString)

Debug.WriteLineで出力される場所

実行すると 次のように出力されます。

たし算です
左辺は value
右辺は 6

ラムダ式部分の Function(value) value + 6 と一致していますね。

式ツリーを自分で構築することもできます。

参考:式ツリー - Visual Basic | Microsoft Docs

 

4-4. 動的なメソッドの書き換え

次の Test クラスの Check プロパティは、1度呼び出すと、自分自身の処理を書き換え、2度目以降は違う動作をします。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class Test
    Public Property Check As Func(Of Integer, Boolean) =
        Function(value)
            Me.Check = Function(value2) value2 > 20
            Return value > 5
        End Function
End Class

1回目は引数が 5より大きければ True を返すラムダ式で、2回目は20より大きければ True を返すラムダ式です。

結果として呼び出し側は1回目の呼び出しと2回目の呼び出しとで結果がかわります。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim t As New Test

'True になります。
Dim result1 As Boolean = t.Check(10)

'False になります。
Dim result2 As Boolean = t.Check(10)

この例は少し面白い発想なので、何か良い活用方法はないかと思われるかもしれませんが、私のプログラム人生の中ではこれをうまく活用できたことはないです。普通に If 文やフラグなどで処理を分岐したり、初級講座では登場していないポリモーフィズムの発想を使う方がはるかにわかりやすいのです。

とはいえ、プロパティにFunctionが代入されている点も含めて興味深くはあります。ちょっと視点を変えるお役には立てたと思いますが、いかがでしょうか。