レキシカル クロージャと query comprehension
Binyam Kelile - Visual Basic チーム
2008 年 3 月
概要
この記事を読むと、レキシカル クロージャ、および Visual Basic 2008 の query comprehension に関連する基本的な概念を学習できます。この記事をよりよく理解するには、LINQ とラムダについての知識が必要です。LINQ とラムダの詳細については、「The LINQ Project」(英語) を参照してください。
対象製品:
Visual Studio 2008Visual Basic
はじめに
レキシカル クロージャ
スコープ
有効期間
スタティック ローカル変数
ジェネリック
有効期間
メソッド呼び出し
MyBase と MyClass
制限事項
まとめ
はじめに
LINQ は Visual Studio 2008 の優れた新機能で、力強さと柔軟性を兼ね備えています。しかし、他のあらゆる新しいテクノロジと同様、"ブラック ボックス" のように感じられる場合がよくあります。というのも、このすばらしい機能を動作させるために内部でなんらかの処理が行われていることはわかりますが、どのような処理が行われているかはわからないからです。この記事は、この謎の一部を明らかにし、クエリがどのように構築され渡されるかを理解できるようにすることを目的としています。
まずは、この記事で使用する標準シナリオについて説明しましょう。ここでは、クラスの生徒のうち、数学のテストで落第点 (50 点未満の点数) を取った生徒を特定する、メモリ内のコレクションをクエリする単純なクエリを使用します。
次のクラスを使用してスキーマを定義しましょう。
例 1
Class Student Public name As String Public mathScore As Integer End Class
実行する例として使用するデータの一部を以下に示します。
例 2
Dim Students() As Student = {New Student With {.name = "Roger", .mathScore = 43}, _ New Student With {.name = "Chris", .mathScore = 37}, _ New Student With {.name = "Sarah", .mathScore = 95}}
上記のようなデータがあれば、全生徒のデータをクエリして、クラスの生徒のうち数学のテストで落第点を取った生徒を取得することができます。
例 3
Dim results = From student In Students _ Where student.mathScore < 50 _ Select student For Each p In results MsgBox(p.name) Next
上記の LINQ コードは問題なくコンパイルして実行することが可能で、数学のテストで落第点を取った生徒の名前を返します。しかし、上記のクエリのしくみは具体的にはどうなっているのでしょうか。
すべてのクエリは、少なくとも 3 つの基本的な概念で構成されています。その 3 つとは、拡張メソッド、ラムダ式、そして、この記事の主なテーマであるクロージャです。この 3 つの基本的な概念の観点から、コンパイラが上記のクエリをどのようにコンパイルするかを説明しましょう。
コンパイラは、クエリを各構成要素部分に分割することによって上記の query comprehension をメソッド呼び出しに変換し、クエリの各部分に埋め込まれた式を取り出し、クエリ中のオブジェクトのメソッド呼び出しで渡されるラムダ関数を生成します。このようなメソッドは、クエリ中のオブジェクトのインスタンス メソッドの場合もあれば、オブジェクトの外部にある拡張メソッドの場合もあり、クエリの実際の実行を実装します。
この例では、変換結果は次のように単純なものです。
例 4
Dim results = Students.Where(Function(student) student.mathScore < 50). _ Select(Function(student) student)
上記の拡張メソッド構文では、ラムダ式を使用しています。ラムダ式は、式の結果を返すインライン関数を表します。ラムダ式はデリゲートに変換され、IEnumerable(ofT) というインターフェイスを実装する特定の型やオブジェクトのインスタンスで使用できる Where 拡張メソッドと Select 拡張メソッドに渡されます。
では、クロージャ ("レキシカル クロージャ" とも呼ばれます) の概念について説明しましょう。
レキシカル クロージャ
レキシカル クロージャ (通称、クロージャ) は、エンド ユーザーの目に直接触れる機能ではなく、記述するコードから直接使用されることを意図したものでもありません。クロージャはコンパイラによって生成されるクラスで、最も外側の関数が含まれるクラスやモジュールの中に入れ子になります。また、他のメソッドが参照する必要があるローカル変数やパラメータが含まれています。
これを明確に示す例を見てみましょう。
例 5
Class ClosureSample Sub Example() Dim passMark As Integer = 50 Dim results = From student In Students _ Where student.mathScore < passMark _ Select student End Sub または Sub Example(ByVal passMark As Integer) Dim results = From student In Students _ Where student.mathScore < passMark _ Select student End Sub End Class
このクエリでは、数学のテストで落第点 (passMark 変数で表される点数 (50 点) 未満の点数) を取った生徒の一覧を取得します。コンパイラは、上記のクエリ ステートメントをメソッド呼び出しに変換すると、まず "student.mathScore < passMark" という式を取り出し、以下に示すラムダ関数を生成します。このラムダ関数は、デリゲートを要求する Where 拡張メソッドに渡され、Where 拡張メソッドでは、このデリゲートを使用して、結果をフィルタ処理します。
例 5(コンパイラによって生成されたコード)
Public Function _Lambda$__1(ByVal student As Student) As Boolean Return (student.mathScore < Me.Local_passMark) End Function
しかし、ここで問題になるのは、どのようにすれば上記のラムダ関数を別のメソッドに渡すことができるかということです。上記のラムダ関数が参照している passMark 変数は、ローカル変数でもなくラムダ関数のパラメータでもありません。このような変数は自由変数と呼ばれます。
このような場合、"変数のリフト" と呼ばれる処理がコンパイラによって背後で行われます。これは、クロージャを使用して passMark 変数の有効期間をローカル関数のスコープよりも長い期間に拡張する処理です。これにより、クエリを実行するのに必要なものが全部用意され、ラムダ式を別のメソッドに渡すこともできるようになります。
ローカル変数の有効期間をローカル関数のスコープよりも長い期間に拡張する必要がある場合にコンパイラによって行われる処理の概要を見てみましょう。コンパイラでは、passMark というフィールドと Where 拡張メソッドに渡されるラムダ式が含まれる次のクロージャ クラスを生成します。このラムダ式では、Sub Example 内にクロージャ クラスの新しいインスタンスを作成し、変数への参照はすべてクロージャ クラスの local_passMark にリダイレクトされます。
例 5(コンパイラによって生成されたコード)
Friend NotInheritable Class ClosureSample Friend Class _Closure$__1Public Sub New(ByVal other As _Closure$__1) If (Not other Is Nothing) Then Me.local_passMark = other.local_passMark End If End Sub Public Function _Lambda$__1(ByVal student As Student) As Boolean Return (student.mathScore < Me.local_passMark) End Function Public local_passMark As Integer End Class Private Shared Function _Lambda$__2(ByVal student As Student) As Student Return student End Function Sub Example() Dim closureVariable_A_8 As New _Closure$__1 closureVariable_A_8.local_passMark = 50 Dim results As IEnumerable(Of Student) = Students.Where(Of Student)(New Func(Of Student, Boolean)(AddressOf closureVariable_A_8._Lambda$__1)).Select(Of Student, Student)(New Func(Of Student, Student)(AddressOf _Lambda$__2)) End Sub End Class
クロージャの定義で説明したように、生成されるクロージャ クラスは、最も外側の関数に含まれるクラス内に入れ子になり、メソッドの外部に存在します。そのため、上記のクエリ ステートメントで作成された Where 拡張メソッドは、クロージャに取り込まれて渡されたローカル変数に、問題なくアクセスすることができます。
したがって、クロージャではクエリの実行に必要なデータをすべてカプセル化します。クロージャ (closure) の概念は、このように、クエリの実行に必要なものをすべて囲い込むこと (enclosure) に由来しています。
スコープ
Visual Basic では、クロージャはスコープの概念に基づいて構築されています。スコープは、変数をプログラム内のどこで使用できるかを決めるものです。リフトされる変数が宣言されている各スコープに対して、コンパイラはそのスコープに関連付けられた新しいクロージャ クラスを生成し、その変数を生成したクロージャ内にリフトします。変数への参照はすべてこのクロージャにリダイレクトされます。
生徒の一覧と各自が取った点数に基づいた段階評価の成績 (A や B など) を返す次のクエリについて考えてみましょう。
例 6
Module Module1 Sub Example(ByVal scoreRange As Integer) Select Case scoreRange Case 80 To 90 Dim gradeB As String = "B" Dim results = From student In Students _ Where student.mathScore >= 80 _ And student.mathScore <= 90 _ Select New With _ {Key .name = student.name, .finalscore = gradeB} Case 95 To 100 Dim gradeA As String = "A" Dim results = From student In Students _ Where student.mathScore >= 95 _ And student.mathScore <= 100 _ Select New With _ {Key .name = student.name, .finalscore = gradeA} End Select End Sub Module
例 6 では、コンパイラは 2 つのクロージャ クラス (各 Case ブロックに対して 1 つのクロージャ クラス) を生成します。生成されたコードがどのようなものか見てみましょう。
例 6(コンパイラによって生成されたコード)
Friend NotInheritable Class Module1 Private Shared Function _Lambda$__1(ByVal student As Student) As Boolean Return ((student.mathScore >= 80) And (student.mathScore <= 90)) End Function Private Shared Function _Lambda$__3(ByVal student As Student) As Boolean Return ((student.mathScore >= &H5F) And (student.mathScore <= 100)) End Function Public Shared Sub Example(ByVal scoreRange As Integer) End Sub Friend Class _Closure$__1 Public Sub New(ByVal other As _Closure$__1) If (Not other Is Nothing) Then Me.local_gradeB = other.local_gradeB End If End Sub Public Function _Lambda$__2(ByVal student As Student) As VB$AnonymousType_0(Of String, String) Return New VB$AnonymousType_0(Of String, String)(student.name, Me.local_gradeB) End Function Public local_gradeB As String End Class Friend Class _Closure$__2 Public Sub New(ByVal other As _Closure$__2) If (Not other Is Nothing) Then Me.local_gradeA = other.local_gradeA End If End Sub Public Function _Lambda$__4(ByVal student As Student) As VB$AnonymousType_0(Of String, String) Return New VB$AnonymousType_0(Of String, String)(student.name, Me.local_gradeA) End Function Public local_gradeA As String End Class End Class
コンパイラによって生成されたコードを見るとわかるとおり、_Closure$__1 と _Closure$__2 が作成されています。
_Lambda$__1 と _Lambda$__3 はリフトされたローカル変数を参照しないので、Where 拡張メソッドに渡されるようにクロージャ クラスの外部に生成されています。したがって、リフトされたローカル変数をラムダ式が参照しない場合は、クロージャ クラスを生成する必要はありません。
そして、各 Case ブロックの先頭でクロージャ インスタンスが作成されます。次にクロージャ インスタンスを使用したクエリの例を示します。
例 6(コンパイラによって生成されたコード)
Public Shared Sub Example(ByVal scoreRange As Integer) Select Case scoreRange Case 80 to 90 Dim Closure_ClosureVariable_14_C As New _Closure$__1 Closure_ClosureVariable_14_C.local_gradeB = "B" Dim results… Exit Select Case 95 to 100 Dim Closure_ClosureVariable_1B_C As New _Closure$__2 Closure_ClosureVariable_1B_C.local_gradeA = "A" Dim results... Exit Select End Select End Sub
例 6 を変更して、入れ子になったブロックや、さまざまな深さのブロックからリフトされる変数の例を確認しましょう。最も外側のブロックと最も内側のブロックで、リフトされる変数が宣言されています。
例 7
Sub Example(ByVal score As Integer) Select Case score Case 90 To 100 Dim gradeB As String = "A" If score >= 95 Then Dim type As String = " With Honor" Dim results = From student In Students _ Where student.mathScore = score _ Select New With _ {Key .name = student.name, _ .finalscore = gradeB, .type = type} End If End Select End Sub
上記の例では、If ブロックが Select...Case ブロック内に入れ子になっており、Select...Case ブロック自体も Example (...) メソッド ブロック内に入れ子になっていて、各ブロックで宣言される変数のスコープはローカルです。この場合、コンパイラは入れ子になったクロージャ クラスを生成します。次にコンパイラによって生成されたコードの例を示します。
例 7(コンパイラによって生成されたコード)
Friend Class _Closure$__1 Public Sub New(ByVal other As _Closure$__1) If (Not other Is Nothing) Then Me.local_score = other.local_score End If End Sub Public local_score As Integer Friend Class _Closure$__2 Public Sub New(ByVal other As _Closure$__2) If (Not other Is Nothing) Then Me.local_gradeB = other.local_gradeB End If End Sub Public local_gradeB As String Friend Class _Closure$__3 Public Sub New(ByVal other As _Closure$__3) If (Not other Is Nothing) Then Me.local_type = other.local_type End If End Sub Public Function _Lambda$__1(ByVal student As Student) As Boolean Return (student.mathScore = Me.closureVariable_10_8.local_score) End Function Public Function _Lambda$__2(ByVal student As Student) As VB$AnonymousType_0(Of String, String, String) Return New VB$AnonymousType_0(Of String, String, String)(student.name, Me.closureVariable_11_C.local_gradeB, Me.local_type) End Function Public local_type As String Public closureVariable_10_8 As _Closure$__1 Public closureVariable_11_C As _Closure$__2 End Class End Class End Class
コンパイラによって生成された上記のコードをよく見ると、_Closure$__3 クラスの _Lambda$__1 関数と _Lambda$__2 関数が、_Closure$__1 の local_score という名前のローカル変数と _Closure$__2 の local_gradeB という名前のローカル変数にそれぞれアクセスを試みていることがわかります。何重もの入れ子になったブロックでは、入れ子のクロージャは再帰的に続くことがわかります。
有効期間
Visual Basic では、ローカル変数の有効期間は、その変数がメモリ内に格納される期間を決定し、その期間はその変数が宣言された関数の有効期間と同じです。しかし、Visual Basic 9.0 では、クロージャにより、ローカル変数の有効期間を関数の有効期間より長くすることができます。
たとえば、数学のテストで 70 点以上の点数を取った生徒の一覧を取得する必要があるとします。この場合、クエリ式を使用して次のようなコードを記述するでしょう。
例 8
Delegate Function Func(ByVal score As Integer) As IEnumerable(Of String) Function Example(ByVal score As Integer) As IEnumerable(Of String) Dim results = From student In Students _ Where student.mathScore > score Select student.name Return results End Function Dim temp As Func = New Func(AddressOf Example)
上記のコードでは、Example 関数内のクエリ式が参照するクロージャに score 変数が追加されています。次の行でこの関数のデリゲートを作成しているので、この関数はそのデリゲートが有効な限り有効となります。そのため、score 変数の有効期間は Example 関数の通常の有効期間よりも長くなります。
スタティック ローカル変数
スタティック ローカル変数は Visual Basic の特別な種類のローカル変数で、これを使用すると、1 回の関数呼び出しから別の関数呼び出しまでの間、値を保持しておくことができます。スタティック ローカル変数の値はプログラムの有効期間中ずっとメモリ内に格納されているので、スタティック ローカル変数はグローバル変数と考えることができます。これは CLR ではサポートされていませんが、コンパイラが、スタティック ローカル変数の値を保持するクラスレベルで共有される変数を作成するという単純な処理によってこれを実現しています。そのため、ローカル スタティック変数はクロージャ内にリフトする必要はありません。次の例について考えてみましょう。
例 9
Class StaticSample Sub Example() Static score As Integer = 97 Dim results = From student In Students _ Where student.mathScore >= score _ Select student.name End Sub End Class
上記の例では、コンパイラはクロージャを生成しませんが、Select 拡張メソッドと Where 拡張メソッドに渡されるラムダ式を生成します。ラムダ式は最も外側の関数のクラス内に生成されます。コンパイラによって生成されたコードは次のようなものです。
例 9(コンパイラによって生成されたコード)
Public Class StaticSample Private Function _Lambda$__1(ByVal student As Student) As Boolean Return (student.mathScore >= Me.score) End Function Private Shared Function _Lambda$__2(ByVal student As Student) As String Return student.name End Function Public Sub Example() Dim results As IEnumerable(Of String) = Students.Where(Of Student)(New Func(Of Student, Boolean)(AddressOf Me._Lambda$__1)).Select(Of Student, String)(New Func(Of Student, String)(AddressOf sample._Lambda$__2)) End Sub Private score As Integer End Class
ご覧のとおり、score 変数の値を保持するクラスレベルで共有される変数が作成されています。そのため、コンパイラでは、クロージャ クラスは作成されず、Where 拡張メソッドと Select 拡張メソッドに渡される _Lambda__1 と _Lambda$__2 が生成されています。
ジェネリック
VB.NET コンパイラは、ジェネリック ローカル変数に対しても、非ジェネリック ローカル変数に対して行うのと同じ処理を行います。次の例について考えてみましょう。
例 10
Sub Example(Of T As {Student, New})(ByVal arg As T) Dim studentObject = arg Dim results = From student In Students _ Where studentObject.name = student.name _ End Sub
この例では、コンパイラはジェネリック クロージャ クラスを生成し、ジェネリック ローカル変数は、このクラス内にリフトされます。次にコンパイラによって生成されるクロージャ クラスの例を示します。
例 10(コンパイラによって生成されたコード)
Friend Class _Closure$__1(Of $CLS0 As { Student, New }) Public Function _Lambda$__1(ByVal student As Student) As Boolean Return (Me.local_studentObject.name = student.name) End Function Public local_studentObject As $CLS0 End Class
コードを見るとわかるとおり、_Closure$__1 という名前のジェネリック クロージャ クラスが生成されており、このクラスには Student および New という型制約が含まれています。
すべてのリフトされるジェネリック変数ごとに、コンパイラによって生成されたクロージャ クラスには、取り込まれた変数と同等のジェネリック パラメータが用意されています。この型パラメータにはジェネリック ローカル変数の宣言に存在する制約がすべて含まれます。
有効期間 shim
Visual Basic では (もちろん、ほとんどのプログラミング言語でも同様ですが)、ローカル変数の有効期間はその変数のスコープと同じではありません。これは CLR のしくみと一致しています。次の例で、Visual Basic のこの動作を確認することができます。この動作の影響を受けるのは、宣言済みだが初期化されていない変数と、GoTo の 2 つです。
例 11
Sub Example() For i = 0 To 2 Dim score As Integer Console.WriteLine(score) score += 2 Next End Sub
上記のコードを実行すると、0、2、および 4 が出力されます。
上記のステートメントを実行すると 0 ばかりが出力されるだろうと思うかもしれません。また、ローカル変数の有効期間がローカル変数のスコープと同じだと思って、For ... Next ブロックの最後に到達したらローカル変数 score のスコープと有効期間の両方が終了すると思うかもしれません。しかし、ローカル変数 score の有効期間は score 自体のスコープと同じではなく、常に関数全体のスコープと同じになります。これは、ローカル変数 score のスコープがブロックレベルの場合でも同様です。ローカル変数 score は明示的に初期化されていないので、作成して 0 に初期化するという処理が 1 回だけ行われます。つまり、ブロックの処理が複数回行われても、ローカル変数では前回の繰り返し処理からの値が保持されます。
しかし、次のように、ローカル変数 score の宣言時に初期化を行うように上記のステートメントを変更すると、0 ばかりが出力されるようになります。
例 12
Sub Example() For i = 0 To 2 Dim score As Integer =0 Console.WriteLine(score) score += 2 Next Sub
上記のコードを実行すると、0、0、および 0 が出力されます。
では、クエリ ステートメントが間にある場合はどうなるでしょうか。0 点を取った生徒と 100 点を取った生徒の数を返す、次の例について考えてみましょう。
例 13
Sub Example() For i = 0 To 1 Dim score As Integer Dim results = From student In Students _ Where student.mathScore = score _ Select student.mathScore Console.WriteLine(score) score += 100 Next End Sub
Visual Basic では、クロージャは、スコープとブロックの概念に基づいて構築されており、そのブロックで定義されている score という名前のリフトされる変数を取り込むので、このような変数のインスタンスは 1 つの関数につき 1 つだけではなくなります。その結果、繰り返し処理が 1 回実行されるたびにクロージャ クラスの新しいインスタンスが作成されるので、出力されるのはすべて 0 となります。
この問題を解決するためにコンパイラが行う処理の概要を説明しましょう。コンパイラは、リフトされる変数が含まれている新しいスコープに入ると、クロージャのインスタンスが既に存在するかどうかを確認します。そして、存在する場合はクロージャの新しいインスタンスを作成し、前のクロージャから保持していた変数の値をリセットします。
コンパイラがこの確認を行うのは、クロージャが生成される関数内でループまたは GoTo が検出された場合のみです。
例 14
Sub Example() For i = 0 To 1 Dim closureVariable_F_8 As _Closure$__1 closureVariable_F_8 = New _Closure$__1(closureVariable_F_8) Dim score As Integer Dim results = From student In Students _ Where student.mathScore = score _ Select student.mathScore Console.WriteLine(score) score += 100 Next End Sub
次にクロージャのコンストラクタの例を示します。
例 15
Public Sub New(ByVal other As _Closure$__1) If (Not other Is Nothing) Then Me.local_score = other.local_score End If End Sub
上記の 2 つの例では、問題を回避するためにコンパイラが行う処理の概要が示されています。
メソッド呼び出し
メソッド呼び出しは、query comprehension 内で行うことができる処理の 1 つです。次に共有メソッドを呼び出す単純な例を示します。
例 16
Class SharedExample Shared Function GetGrade(ByVal score As Integer) As String Select Case score Case 90 To 100 Return "A" Case 70 To 89 Return "B" Case Else Return "C" End Select End Function End Class Sub Example() Dim results = From student In Students _ Select New With {Key .name = student.name, _ .grade = SharedExample.GetGrade(student.mathScore)} End Sub
モジュール内または共有メソッド内で定義されたメソッドは、通常のメソッドと違って、オブジェクトのインスタンスを使用せずに直接呼び出すことができます。そのため、クロージャを作成する必要はありません。
しかし、インスタンス メソッドの場合は、コンパイラは、ローカル変数をリフトする場合と同様に Me をクロージャ内にリフトします。
例 17
Class InstanceExample Function GetGrade(ByVal score As Integer) As String Select Case score Case 90 To 100 Return "A" Case 70 To 89 Return "B" Case Else Return "C" End Select End Function Sub Example(ByVal courseName As String) Dim results = From student In Students _ Select New With {Key .name = student.name, _ .courseName = courseName, _ .grade = GetGrade(student.mathScore)} End Sub End Class
上記の例では、コンパイラはクロージャを生成し、Me と courseName を生成したクロージャ内にリフトします。次にコンパイラによって生成されたクロージャのコードの例を示します。
例 17(コンパイラによって生成されたコード)
Public Class InstanceExample Friend Class _Closure$__1 Public Sub New(ByVal other As _Closure$__1) If (Not other Is Nothing) Then Me.$VB$Me = other.$VB$Me Me.local_courseName = other.local_courseName End If End Sub Public Function _Lambda$__1(ByVal student As Student) As VB$AnonymousType_0(Of String, String, String) Return New VB$AnonymousType_0(Of String, String, String)(student.name, Me.local_courseName, Me.$VB$Me.GetGrade(student.mathScore)) End Function Public local_courseName As String Public $VB$Me As InstanceExample End Class End Class
もちろん、query comprehension 内でインスタンス メソッドを呼び出すコードを記述しても、コンパイラによってコード内でのメソッドの使われ方を確認してクロージャを生成するかどうかが判断される場合もあります。たとえば、次の例の InstanceExample クラス内にある query comprehension について考えてみましょう。
例 18
Class InstanceExample Function GetGrade(ByVal score As Integer) As String Select Case score Case 90 To 100 Return "A" Case 70 To 89 Return "B" Case Else Return "C" End Select End Function Sub Example() Dim results = From student In Students _ Select New With {Key .name = student.name, _ .grade = GetGrade(student.mathScore)} End Sub End Class
上記のクエリ式では、Select 拡張メソッドの引数として渡すことができる、コンパイラによって生成されたラムダ式が参照する必要があるローカル変数を Example メソッド内で参照していません。そのため、上記の例では、クロージャは生成されません。したがって、コンパイラは、クロージャと呼ばれる 1 つのラッパー クラスを生成する代わりに、生成されたラムダ式を InstanceExample クラス内に配置します。コンパイラによって生成された次のコードは、上記の query comprehension に対してコンパイラが行う処理の概要を示します。
例 18(コンパイラによって生成されたコード)
Public Class InstanceExample Private Function _Lambda$__1(ByVal student As Student) As AnonymousType_0(Of String, String) Return New AnonymousType_0(Of String, String)(student.name, Me.GetGrade(student.mathScore)) End Function Public Sub Example() Dim results As IEnumerable(Of AnonymousType_0(Of String, String)) = Students.Select(Of Student, AnonymousType_0(Of String, String))(New Func(Of Student, AnonymousType_0(Of String, String))(AddressOf Me._Lambda$__1)) End Sub End Class
上記のコード例からわかるように、コンパイラによってコードが最適化される場合があります。
MyBase と MyClass
MyBase や MyClass も query comprehension 内で行うことができる処理で、これらはクロージャに取り込まれます。MyBase を使用して、クエリ式内で、基本クラスのメソッドをオーバーライドする関数から、基本クラスのメソッドを呼び出す例を見てみましょう。
例 19
Module Module1 Public Class Base Public Overridable Function Example(ByVal score As Integer) As String Select Case score Case 90 To 100 Return "A" Case 70 To 89 Return "B" Case Else Return "C" End Select End Function End Class Class Derived Inherits Base Public Overrides Function Example(ByVal score As Integer) As String Return Nothing End Function Public Function Sample(ByVal courseNumber As String) As String Dim results = From student In Students _ Select New With {Key .name = student.name, _ .course = courseNumber, _ .grade = MyBase.Example(student.mathScore)} End Function End Class End Module
MyBase をサポートするためにコンパイラが背後で行っている処理を説明しましょう。コンパイラは、メソッドを生成し、そのメソッド内で MyBase を呼び出しています。次に、クロージャとラムダ式を生成しています。ラムダ式は、このメソッドを使用して Me と courseNumber をクロージャ内にリフトする Select 拡張メソッドに渡されます。次にコンパイラによって生成されたコードの例を示します。
例 19(コンパイラによって生成されたコード)
Friend NotInheritable Class Module1 Public Class Base Public Overridable Function Example(ByVal score As Integer) As String Dim t_i0 As Integer = score If (IIf(((t_i0 >= 90) AndAlso (t_i0 <= 100)), 1, 0) <> 0) Then Return "A" End If If (IIf(((t_i0 >= 70) AndAlso (t_i0 <= &H59)), 1, 0) <> 0) Then Return "B" End If Return "C" End Function End Class Public Class Derived Inherits Base Public Function Example_MyBase(ByVal p0 As Integer) As String Return MyBase.Example(p0) End Function Public Overrides Function Example(ByVal score As Integer) As String Return Nothing End Function Public Function Sample(ByVal courseNumber As String) As String Dim Sample As String Dim closureVariable_23_C As New _Closure$__1 closureVariable_23_C.$VB$Me = Me closureVariable_23_C.courseNumber = courseNumber Dim results As IEnumerable(Of VB$AnonymousType_0(Of String, String, String)) = Module1.Students.Select(Of Student, VB$AnonymousType_0(Of String, String, String))(New Func(Of Student, VB$AnonymousType_0(Of String, String, String))(AddressOf closureVariable_23_C._Lambda$__1)) Return Sample End Function Friend Class _Closure$__1 Public Function _Lambda$__1(ByVal student As Student) As VB$AnonymousType_0(Of String, String, String) Return New VB$AnonymousType_0(Of String, String, String)(student.name, Me.$VB$Local_courseNumber, Me.$VB$Me.Example_MyBase(student.mathScore)) End Function Public courseNumber As String Public $VB$Me As Derived End Class End Class
今度は、MyClass を使用する例を見てみましょう。MyClass を使用すると、現在のインスタンスのオーバーライド可能なメソッドをクエリ式内で呼び出すことができます。
例 20
Module Module1 Public Class Base Public Overridable Function Example(ByVal score As Integer) As String Select Case score Case 90 To 100 Return "A" Case 70 To 89 Return "B" Case Else Return "C" End Select End Function Public Sub Sample(ByVal courseNumber As String) Dim results = From student In Students _ Select New With {Key .name = student.name, _ .course = courseNumber, _ .grade = MyClass.Example(student.mathScore)} End Sub End Class Class Derived Inherits Base Public Overrides Function Example(ByVal score As Integer) As String Return Nothing End Function End Class End Module
MyBase の場合と同様に、コンパイラはメソッドを呼び出し、そのメソッド内で MyClass を呼び出します。次に、クロージャとラムダ式を生成します。ラムダ式は、このメソッドを使用して Me と courseNumber をこのクロージャ内にリフトする Select 拡張メソッドに渡されます。次にコンパイラによって生成されたコードを示します。
例 20(コンパイラによって生成されたコード)
Friend NotInheritable Class Module1 Public Class Base Public Function Example_MyClass(ByVal p0 As Integer) As String Return Me.Example(p0) End Function Public Overridable Function Example(ByVal score As Integer) As String Dim t_i0 As Integer = score If (IIf(((t_i0 >= 90) AndAlso (t_i0 <= 100)), 1, 0) <> 0) Then Return "A" End If If (IIf(((t_i0 >= 70) AndAlso (t_i0 <= &H59)), 1, 0) <> 0) Then Return "B" End If Return "C" End Function Public Sub Sample(ByVal courseNumber As String) Dim closureVariable_35_C As New _Closure$__1 closureVariable_35_C.courseNumber = courseNumber closureVariable_35_C.$VB$Me = Me Dim results As IEnumerable(Of VB$AnonymousType_0(Of String, String, String)) = Module1.Students.Select(Of Student, VB$AnonymousType_0(Of String, String, String))(New Func(Of Student, VB$AnonymousType_0(Of String, String, String))(AddressOf closureVariable_35_C._Lambda$__1)) End Sub Friend Class _Closure$__1 Public Function _Lambda$__1(ByVal student As Student) As VB$AnonymousType_0(Of String, String, String) Return New VB$AnonymousType_0(Of String, String, String)(student.name, Me.courseNumber, Me.$VB$Me.Example_MyClass(student.mathScore)) End Function Public courseNumber As String Public $VB$Me As Base End Class End Class Public Class Derived Inherits Base Public Overrides Function Example(ByVal score As Integer) As String Return Nothing End Function End Class End Class
上記の 2 つの例は、query comprehension 内で MyBase や MyClass を使用するためにコンパイラが行う処理の概要を示すものです。
制限事項
変数のリフトに関して、知っておく必要がある制限がいくつかあります。
ByRef パラメータ
query comprehension 内で ByRef パラメータを参照すると、コンパイル時にエラーが発生します。ByRef パラメータを使用すると、呼び出されたルーチン内で引数の値を変更できるからです。そのため、ByRef 変数の有効期間を任意に拡張することはできません。
例 21
Sub Example(ByRef score As Integer) Dim results = From student In Students _ Where student.mathScore < score Order By student.name _ Ascending Select student.name End Sub
上記の query comprehension を実行すると、コンパイラから次のエラーが返されます。
"エラー BC36533: 'ByRef' パラメータ 'arg' をクエリ式で使用することはできません。"
Me と構造体
クロージャでは Me が含まれるリフトされる変数の有効期間が拡張されるので、構造体の中の query comprehension では Me をリフトすることができません。また、クロージャは参照によって値を保持し、構造体はスタック上に作成されるので、参照によって構造体の Me をリフトして有効期間を拡張することはできません。
例 22
Structure Struct Private age As Integer Public Sub Example() Dim results = From student In Students Select _ New With {.age = age, .name = student.name} End Sub End Structure
そのため、上記のクエリ ステートメントを実行すると、コンパイル時に次のエラーが発生します。
"BC36535: インスタンス メンバおよび 'Me' を構造体のクエリ式内で使用することはできません。"
GoTo
GoTo を使用して、クロージャが含まれるスコープに移動することはできません。
例 23
Sub Example() GoTo lable1 While True Dim score As Integer = 90 lable1: Dim results = Aggregate student In Students _ Where student.mathScore > score Into Count() Exit While End While End Sub
「有効期間」で紹介した例では、コンパイラはここでもう 1 つ追加の処理を行う必要があります。ローカル変数 score にアクセスする前にクロージャを初期化するという処理です。GoTo を使用して、クロージャが含まれるスコープに移動できてしまうと、コンパイラで、この初期化処理を行うのが困難になります。そのため、上記のクエリ ステートメントを実行すると、コンパイル時に次のエラーが発生します。
"エラー BC36597: 'lable1' は、ラムダ式またはクエリ式で使用される変数を定義するスコープの内側にあるため、'GoTo lable1' は有効ではありません。"
制限がある型
CLR では、配置できる場所や使用方法に基づいた制限のある型がいくつかあります。この制限により、このような型は、制限に反したクラスのメンバ フィールドとして宣言したり使用したりすることができません。また、このような型の変数をリフトすることはできません。リフトしようとすると、コンパイル時にエラーが発生します。
例 24
Sub Example() Dim arg As New ArgIterator Dim results = From student In Students _ Where arg.GetRemainingCount > 0 Select student.name End Sub
上記のクエリ式を実行すると、コンパイラから次のエラーが返されます。
"エラー BC36598: 制限がある型 'System.ArgIterator' のインスタンスをクエリ式で使用することはできません。"
まとめ
query comprehension は、プロジェクション、選択、外積、グループ化、並べ替えなどのさまざまな定義済みクエリ演算子キーワードを含む基本的なクエリ機能を備えた、簡潔で構造化されたクエリ式を提供します。query comprehension を使用すると、SQL に似たクエリを VB コード 内で直接記述することができ、IDE の強化された IntelliSense 機能も活用できます。また、query comprehension は、型の推論、拡張メソッド、ラムダ式、匿名型、レキシカル クロージャなど、他の新機能でサポートされています。この記事の主なテーマであるレキシカル クロージャは、直接ユーザーの目に触れる機能ではなく、記述するコードから直接使用されることを意図したものでもなく、内部的なコンパイル機能です。しかし、コンパイラが背後でどのような処理を行っているのかを知り、理解することは、簡潔かつ正確で高速なクエリ ステートメントを VB.NET コード内で直接記述するのに役立ちます。
Binyam Kelile は、マイクロソフトの Visual Basic テスト チームのソフトウェア開発エンジニアです。