Visual Basic 初級講座 [改訂版] |
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
Visual Basic 中学校 > 初級講座[改訂版] >
2021/5/30
この記事が対象とする製品・バージョン
![]() |
Visual Basic 2019 | ◎ | 対象です。 |
![]() |
Visual Basic 2017 | ◎ | 対象です。 |
![]() |
Visual Basic 2015 | ◎ | 対象です。 |
![]() |
Visual Basic 2013 | ◎ | 対象です。 |
![]() |
Visual Basic 2012 | ◎ | 対象です。 |
![]() |
Visual Basic 2010 | ◎ | 対象です。 |
![]() |
Visual Basic 2008 | ○ | 対象ですが、VB2008では使えない機能も扱っています。 |
![]() |
Visual Basic 2005 | × | 対象外です。ラムダ式はVB2008から導入された機能です。 |
![]() |
Visual Basic.NET 2003 | × | 対象外です。 |
![]() |
Visual Basic.NET (2002) | × | 対象外です。 |
![]() |
Visual Basic 6.0 | × | 対象外です。 |
目次
ラムダ式の外側で定義されている変数をラムダ式の中で使用することができます。
次のプログラムは、ラムダ式の外側で定義されている変数xとyを、ラムダ式の中で使っています。
Dim x As Integer = 2
Dim y As Integer = 3
Dim add = Function() x + y
変数とラムダ式のスコープが違う場合でも動作します。
次のようなケースです。
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がコンパイル時に自動生成しているので、プログラムからは通常見たりアクセスすることはできませんが、このようなものが存在するということを覚えておく必要はあります。
数値型のような単純なものならキャプチャーされてもあまり問題になりませんが、終了処理が必要なオブジェクトがキャプチャーされた場合、期待したタイミングで終了処理が実行できないかもしれません。たとえば、ラムダ式の外側にあるデータベースへの更新や、ファイルへの書き込みを行うオブジェクトをラムダ式がキャプチャーした場合、更新の完了や書き込みの終了のタイミングの制御が難しくなります。(難しいだけではなく、キャプチャーされていることに気が付かないでバグの原因になってしまうかもしれません。)
VBは、内部で起こっていることをできるだけプログラマーの目から隠して、プログラマーが期待している通りにプログラムが動くように見せかけます。
少し実験してみましょう。 次の関数は引数で与えられた数字(number)を2乗するラムダ式を返します。。。
※2乗とは、2回かけ算するという意味で、3の2乗は9, 4の2乗は16, 10の2乗は100という具合です。
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 パラメーターはクロージャにキャプチャーされます。
これを実際に呼び出す例は次のようになります。
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 が表示されます。
実験はここからです。
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 number As Integer とするとどうなるでしょうか。これだと、 x と number は同じモノということになるのですが、今度は VB がエラーにします。「エラー BC36639 'ByRef' パラメーター 'number' をラムダ式で使用することはできません。 」という出力されます。
この状態だと複雑すぎるので、変数のキャプチャーを実行しないで、エラーにされてしまうというわけです。
それでは、今度は次のようにしてみましょう。
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つあって、1つ目はStringで2つ目はIntegerで戻り値は Double であるというような定義をするための型です。ですから、ラムダ式を表現するのにぴったりというわけです。Delegateキーワード(読み方:Delegate=デリゲート)を使うと自分でデリゲートを定義することもできます。
1つ目の引数がString、2つ目の引数がIntegerで戻り値がDouble であるメソッドは次のようなデリゲート型で表現できます。
Public Delegate Function SampleDelegate(p1 As String, p2 As Integer) As Double
デリゲートは型なので、この型の変数を使って利用します。その変数には、このデリゲートと定義が一致するメソッドを代入して、呼び出すことができます。
デリゲートにメソッドを代入するときには、AddressOf演算子(読み方:AddressOf=アドレスオブ)を使うのが特徴的です。
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以降はデリゲートの出番はほとんどありません。
たとえば、上述の例はラムダ式を使って次のように書き換えることができます。
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 というキーワードがなくなり、行数も減って、だいぶシンプルに感じます。状況によってはさらに次のように簡略化することもできます。
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年現在では、デリゲートについては、そういうものもあるという程度のことを知っていればよく、ラムダ式の方を活用するのが良いでしょう。
デリゲートにはラムダ式との互換性もあり、同じ定義のラムダ式を代入することもできます。
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を使ってもっと簡単に書けることは前回説明しています。次のようになります。
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に推論させて、どのような型になったか確認できます。
Dim s1 = Sub() Debug.WriteLine("TEST")
Debug.WriteLine("型の名前:" & s1.GetType.FullName)
If TypeOf s1 Is [Delegate] Then
Debug.WriteLine("s1 は デリゲートです。") 'これが表示されます。
End If
実行すると 次のように出力されます。
型の名前: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)
そもそも、型の名前に使えない記号 $ がついているので、構文上も無理があります。
ラムダ式を使用しているときは、普段あまり意識していない ( ) に注意が必要です。
次の例はラムダ式を実行し、変数 result の値は 627 になります。
'固定で627を返すラムダ式
Dim lambda = Function() 627
'result は 627 になります。
Dim result = lambda()
最後の行で lambda() と ( ) がついていることに注意してください。
この ( ) があるから、ラムダ式で定義している処理が実行され戻り値が reuslt に代入されるのです。
一方、次のプログラムは lambda の後ろに ( ) がついていません。
'固定で627を返すラムダ式
Dim lambda = Function() 627
'result は 627 を返すラムダ式になります。
Dim result = lambda ' ← ( ) がついていないのでラムダ式は実行されません。
'value は 627 になります。
Dim value = result()
結果として、result はラムダ式自身を表すことになり、result( ) と記述することでラムダ式の処理を実行できるようになります。もちろん、lambda( ) と書いても同じです。
このように ( ) の有無でまるで意味が変わります。
ラムダ式の戻り値が配列やコレクションの場合は、もっと紛らわしいです。配列やコレクションの要素を取得するときにも ( ) を使うので、これとの区別がちょっとだけややこしいからです。
'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} のように、ラムダ式の配列も書けるので、( ) の使い方を複雑にしようと思えば、いろいろと細工はできます。
今のところ、このようなわかりにくいラムダ式の使い方を私は見たことはありません。
ラムダ式はこれまでに説明した以外にもいろいろな活用方法があります。初級講座では深入りしないつもりの部分もありますが、簡単に紹介します。
ラムダ式を使うと簡単に非同期実行・マルチスレッドのプログラムを実現することができます。
次の例はWindowsフォームアプリケーションで実行すると MsgBox を2つ同時に表示します。通常MsgBoxは閉じられるまで次のプログラムが実行されないので2つ同時に表示することはできませんが、マルチスレッドであれば実現できます。
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"))
LINQのクエリ式を使って、集合を操作することができます。条件を指定した抽出や、並び替えや集計などさまざまなことができます。LINQについては今後初級講座で取り上げる予定です。
Dim names = New String() {"徳川家康", "織田信長", "徳川吉宗", "豊臣秀吉", "徳川慶喜"}
'「徳川」から始まるものの抽出を定義 = LINQのクエリ式
Dim tokugawas = From name In names Where name.StartsWith("徳川")
'抽出を実行し、結果を表示
tokugawas.ToList.ForEach(Sub(name) Debug.WriteLine(name))
この例のLINQのクエリ式は下記のプログラムの書き換えです。形を変えたラムダ式というわけです。
Dim names = New String() {"徳川家康", "織田信長", "徳川吉宗", "豊臣秀吉", "徳川慶喜"}
'「徳川」から始まるものの抽出を定義 = LINQのクエリ式
Dim tokugawas = names.Where(Function(name) name.StartsWith("徳川"))
'抽出を実行し、結果を表示
tokugawas.ToList.ForEach(Sub(name) Debug.WriteLine(name))
一行形式のラムダ式は簡単に式ツリーに変換することができます。
'▼この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)
実行すると 次のように出力されます。
たし算です
左辺は value
右辺は 6
ラムダ式部分の Function(value) value + 6 と一致していますね。
式ツリーを自分で構築することもできます。
参考:式ツリー - Visual Basic | Microsoft Docs
次の Test クラスの Check プロパティは、1度呼び出すと、自分自身の処理を書き換え、2度目以降は違う動作をします。
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回目の呼び出しとで結果がかわります。
Dim t As New Test
'True になります。
Dim result1 As Boolean = t.Check(10)
'False になります。
Dim result2 As Boolean = t.Check(10)
この例は少し面白い発想なので、何か良い活用方法はないかと思われるかもしれませんが、私のプログラム人生の中ではこれをうまく活用できたことはないです。普通に If 文やフラグなどで処理を分岐したり、初級講座では登場していないポリモーフィズムの発想を使う方がはるかにわかりやすいのです。
とはいえ、プロパティにFunctionが代入されている点も含めて興味深くはあります。ちょっと視点を変えるお役には立てたと思いますが、いかがでしょうか。