レキシカル クロージャと 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 テスト チームのソフトウェア開発エンジニアです。