Export (0) Print
Expand All

Lexical Closure and Query Comprehension

Visual Studio 2008 Technical Articles


Binyam Kelile—Visual Basic Team
Microsoft Corporation

Summary: This article enables you to get an overview of Lexical Closure and the basic concept involved with Query Comprehension in Visual Basic 2008. A previous knowledge of LINQ and Lambda is required to get a better understanding of this article. If you need to read more about LINQ and Lambda click here.

(23 printed pages)

Contents

    Introduction
    Lexical Closures
    Scope
    Lifetime
    Static Locals
    Generics
    Lifetime shim
    Method Call
    MyBase/MyClass
    Restrictions
    Conclusion

Introduction

Linq is an excellent new capability in VS2008, powerful and flexible at the same time. But like any new technology, it can often seem like a 'black box' in that you know something is going on under the hood to make all this cool stuff work, you just do not know what! The purpose of this article is to push aside some of the mystery and give you an insight into how queries are constructed and passed around.

First, let's set up a standard scenario we are going to work with, a simple query which queries an in-memory collection that determines the students in the class who failed in math (a score of less than 50).

Let's define the schema by using the following class.


Example 1


    Class Student
        Public name As String
        Public mathScore As Integer
    End Class
    

Here is a small subset of data that we will use as our running example:


Example 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}}
	                    		                    	

Given the above data, we can query for all students to get the students in the class who failed in math.


Example 3


        Dim results = From student In Students _
                      Where student.mathScore < 50 _
                      Select student
                      
        For Each p In results
            MsgBox(p.name)
        Next

The above LINQ code compiles and runs fine, returning us the names of those students who failed in math. But what exactly is the query above?

Well, all queries are composed of at least three basic concepts: an extension method, lambda expression and closure, which is the main focus of this article. In terms of these basic concepts, here is how the compiler compiles the above query.

The compiler translates the above query comprehension into invocation of method call by splitting the query up into its component parts, pulls the expression embedded in each part of the query, and generates a lambda function that gets passed into method calls of the object being queried. The methods can be instance methods of the object being queried or extension methods that are external to the object which implement the actual execution of the query.

In this case, the translation could be as simple as the following:


Example 4


     Dim results = Students.Where(Function(student) student.mathScore < 50). _
                           Select(Function(student) student) 
  
  

The above extension method syntax depends on lambda expressions, which represent inline functions that return the result of an expression. The lambda expression is converted to a delegate and passed to Where and Select extension methods that are available on an instance of a particular type or object that implements an interface called IEnumerable(of T).

Now let's discuss the concept of closure (also known as a 'Lexical Closure').

Lexical Closures


A Lexical Closure, often referred to as a closure, is not an immediately visible feature to an end user and is not intended to be used directly from your code. But rather it is a compiler-generated class, which is nested in the containing class or module of the outermost function, and contains the local variables or parameters that need to be referred to by other methods.

Let's see an example that will make this clear:


Example 5


     Class ClosureSample
         Sub Example()
             Dim passMark As Integer = 50
             Dim results = From student In Students _
                           Where student.mathScore < passMark _
                           Select student
         End Sub
 Or
         Sub Example(ByVal passMark As Integer)
             Dim results = From student In Students _
                           Where student.mathScore < passMark _
                           Select student
          End Sub
      End Class


This query retrieves the list of students who failed on math that represented by passMark variable (a score of 50) and when the compiler translates the above query statement into invocation of method call, it first pulls the expression "student.mathScore < passMark" out and generates the following lambda function, which gets passed to the Where extension method that expects a delegate. Then, the Where extension method uses this delegate to filter out the result.


Example 5 — Compiler Generated


   Public Function _Lambda$__1(ByVal student As Student) As Boolean
       Return (student.mathScore < Me.Local_passMark)
   End Function

But the question here will be how we can pass the above lambda function into another method, when the "passMark" variable referred by the above lambda function that is not a local variable or parameter of the lambda function, we call it free variable.

Here is the compiler behind-the-scenes trick, commonly known as variable lifting, that extends the lifetime of the "passMark" variable beyond the local function scope using closure, which represents the entirety of everything we need to run a query that facilitates passing the lambda function into another method.

Let's see what the compiler basically does when it needs to extend the lifetime of a local variable beyond the local function scope. Generate the following closure class, which has a field "passMark" and a lambda expression that gets passed to Where extension method and creates a new instance of a closure class inside the Sub Example, and then redirects all variable references into the "local_passMark" of the closure class.


Example 5 — Compiler Generated


  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

As I mentioned in the closure definition, the generated closure class lives outside of the method nested in the containing class of the outermost function. Therefore, the Where extension methods created by the above query statement can legally access the local variables captured in the closure and passed into.

Therefore, closure encapsulates all of the data needed for the query to run. This is where the closure concept comes from: the enclosure of everything necessary for the query to run.


Scope

Closure is structured in Visual Basic based on the concept of scope, which decides where the variables can be used in a program. And for every scope where lifted variables declared, the compiler generates a new "closure" class associated with that scope and lifts the variable into the same closure and all references to the variables are redirected to that closure.

Consider the following query that returns list of students and their letter grade based on their score:


Example 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

 

In Example 6, the compiler will generate two closure classes for each "Case" block. Let's see what the generated code looks like:


Example 6 — Compiler Generated


  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

As you can see in the compiler generated code, _Closure$__1 and _Closure$__2 are created.

Notice that _Lambda$__1 and _Lambda$__3 are generated to get passed to Where extension method outside of the closure class, because they don't refer any lifted local. Therefore, there is no need to generate a closure class if the lambda expression doesn't refer any lifted local.

Then a closure instance will be created at the beginning of each "Case" block. Here is what the query with the closure instance looks like:


Example 6 — Compiler Generated


    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


Let's modify the previous example to take a look at the case of nested block or variable lifted from different levels of enclosing blocks, where the outermost and innermost blocks declare lifted variable.


Example 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


From the above example "If" block is nested within "Select...Case" block and "Select...Case" block itself is nested inside Example (...) method block and, the variables declared in each block will have local scope. In this case, the compiler generates a nested closures class. Here is how the compile code looks like:


Example 7 — Compiler Generated


    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


If you closely look at the above compiler generated code, you can see that the "_Closure$__3" class; "_Lambda$__1" and "_Lambda$__2" functions are trying to gain access to the "_Closure$__1" and "_Closure$__2" local variables called "local_score" and "local_gradeB" respectively. Notice that for deep nested blocks nesting closure will continue recursively.


Lifetime

The lifetime of local variables in Visual Basic determines how long the storage for that variable exists in memory that is equal to the lifetime of the function where it is declared. But, in Visual Basic 9.0, closures allow the lifetime of a local variable to be extended beyond the lifetime of the function.

Imagine I want a list of students who got above 70 in math exam. Using query expression I would write like this:


Example 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)

  

In the preceding code "score" is added to the closure referenced by the query expression inside Example function. Creating a delegate in the next line to this function keeps it alive as long as the delegate is alive and hence extends the lifetime of "score" beyond the normal lifetime of Example function.


Static Locals

A static local is a special type of local variable in Visual Basic, which allows a value to be retained from one call of a function to other. This can be thought of as a global variable as its value remains in memory for the lifetime of the program. The CLR does not support this, but a compiler does this with just a simple compiler trick by creating a shared class-level variable to maintain the value of the static local variable. As a result the variable does not need to be lifted into the closure. Consider the following example:


Example 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
  

In the preceding example compiler doesn't generate closure, but it does generate a lambda, that gets passed into the Select and Where extension methods. The generated lambda will be inside the outermost function class and the compiler generated code looks like the following:


Example 9 — Compiler Generated


  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
  

As you can see, the compiler created a shared class level variable to maintain the value of "score". Therefore, the compiler didn't generate a closure class instead it did generate "_Lambda__1" and "_Lambda$__2" that gets passed into Where and Select extension methods.


Generics

VB.NET compiler does the same thing for generic locals as what it does for non-generics. Consider the following example:


Example 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
  

Here, the compiler will generate a generic closure class, where a generic local gets lifted. The compiler generated closure class looks like the following:


Example 10 — Compiler Generated


  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
  

As you can see in the code, the compiler generated a generic closure class called _Closure$__1, including the type constraints "Student" and "New".

Note that for every generic lifted variable, the compiler-generated closure class will have generic parameters equal to the captured variables and these type parameters will include any constraints that might be present on the generic local declaration.


Lifetime Shim

In Visual Basic and, of course, in most programming languages, the lifetime of a local variable is not the same as its scope, which is consistent with the way CLR works. You can simply observe this behavior in Visual Basic, that affects two parts of the language, i.e. declared but uninitialized variables and GoTo, with the following example:


Example 11


  Sub Example() 
      For i = 0 To 2
          Dim score As Integer
              Console.WriteLine(score)
              score += 2
      Next
  End Sub


The above code will print out: 0, 2, and 4.

You may think that the above statement would print out all 0's. Assuming that the lifetime of a local variable is the same as the scope of the local variable and you may expect that when the code reaches the end of the "For ... Next" block, both the scope and the lifetime of the local variable "score" ends. However, the lifetime of a local variable "score" is not the same as its scope instead it is always that of the entire function, even if the local variable "score" scope is block-level. Since "score" is not explicitly initialized, it would be created and initialized to 0 only once. Therefore, this implies that even if a block is entered more than once, the value of a local variable "score" will be retained from the previous iteration.

But if we include initialization when we declare local variable "score", the above statement would print out all 0's:

Example 12


  Sub Example()
      For i = 0 To 2
          Dim score As Integer =0
          Console.WriteLine(score)
          score += 2
      Next
  Sub


The above code will print out: 0, 0, and 0.

But, what would happen if there is a query statement in between? Consider the following example that returns a number of students who scored 0 and 100.


Example 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


Since Closures are structured in Visual Basic based on the concept of scope/blocks, that would capture lifted variables called "score" declared in that block, there will no longer be a single instance of those variables per function, and as the result, every iteration will cause a new instance of the closure class to be created and the output would be all 0's.

But to fix this issue here is what the compiler basically does, when it enters a new scope containing a lifted variable, the compiler will check to see if an instance of closure already exists; if so, the compiler will create a new instance of closure and reset the value of the variable from the previous closure.

Note that the compiler only does the above check if it detects a loop or a GoTo in the function where the closure is generated.


Example 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


And here is what the closure constructor looks like:


Example 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


So, the preceding examples show you what the compiler basically does to get around the issue.


Method Call

Method call is one of the possible things to do inside query comprehension. The following is a simple example of calling shared method.


Example 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


Method declared inside a Module or Shared method doesn't need an object instance like a regular method, but rather it is called directly. Therefore, there is no need to create a closure.

But, for instance method, the compiler lifts "Me" into closure as what it does for local variable lifting.


Example 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


In the above case, the compiler generates closure and lifts "Me" and "courseName" into that closure. And here is what the compiler generated closure code looks like.


Example 17 — Compiler Generated


    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


There are cases, of course, where the compiler is smart enough to see through your use and determine in either to generate closure or not, even if you call instance method inside Query Comprehension. For example, consider the following Query Comprehension inside "InstanceExample" class:


Example 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


In the above case the compiler doesn't generate closures, because the above query expression doesn't refer to any local variables inside Example method that need to be referred to by the compiler-generated lambda expression that can be passed as an argument for the "Select" extension method. Therefore, instead of emitting another wrapper class called closure, the compiler places the generated lambda expression inside an "InstanceExample" class. The following compiled code shows what the compiler basically does for the above query comprehension:


Example 18 — Compiler Generated


  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

Therefore, as you can see, in the previous code example, there are cases where the compiler performs code optimization.


MyBase/MyClass

MyBase or MyClass are the other possible things to do inside Query Comprehension and will be captured into a closure. Let's see an example that uses MyBase to call to the base class method inside a query expression from a function that overrides this method.


Example 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


Here is what the compiler does behind the scenes to support "MyBase". It generated a method and called mybase inside that method, and then it generated closure and lambda that gets passed to the Select extension method which uses this method and lifts "Me" and "courseNumber" into that closure. The compiler-generated code look like the following:


Example 19 — Compiler Generated


  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


Let's see another example for the case of "MyClass" that allows you to call an overridable method of the current instance inside a query expression:


Example 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


In the same way as for "MyBase", the compiler generates a method and calls "MyClass" inside that method, and then generates closure and lambda that gets passed to Select extension method which uses this method and lifts "Me" and "courseNumber" into that closure.  Below is the compiler generated code:


Example 20 — Compiler Generated


  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


The preceding examples are to show you what the compiler basically does to use "MyBase" or "MyClass" inside Query Comprehension.


Restrictions

There are some restrictions you should be aware of regarding lifting variables.


ByRef Parameters

Any reference to a ByRef parameter inside query comprehensions will cause a compile time error. Because, a ByRef parameter allows the value of the argument to be changed in the calling routine. Therefore, it is not legal to arbitrarily extend the lifetime for a ByRef variable.


Example 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


For the above query comprehension, compiler generates the following error:

"error BC36533: 'ByRef' parameter 'arg' cannot be used in a query expression."

 


"Me" and Structures

Query Comprehension inside a structure will not be allowed to lift "Me", because closure will extend the lifetime of the lifted variable that includes "Me". But, Closure will hold value by reference and also structure is created on the stack. So, it's not possible to lift "Me" of the structure by reference and extend its lifetime in that manner.


Example 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


Therefore, the above query statement will cause a compile-time error:

"BC36535: Instance members and 'Me' cannot be used within query expressions in structures."

 

GoTo

Compiler doesn't allow to GoTo into scope that contains closure.


Example 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


In my earlier example, in the Lifetime shim section, there is an extra work the compiler has to do here. The closure has to be initialized before the local variable "score" is accessed. Therefore, allowing this will make it difficult for the compiler to maintain the previous task. As a result, the above query statement will cause a compile-time error:

"error BC36597: 'GoTo lable1' is not valid because 'lable1' is inside a scope that defines a variable that is used in a lambda or query expression."

 

Restricted Types

There are some types in the CLR that are restricted types based on where they can be placed and how they can be used, and this placement restriction prevents them from being declared or used as a member field in a class as such. It is not possible to lift this variable. Trying to do so will result in a compile-time error.


Example 24


     Sub Example()
         Dim arg As New ArgIterator
         Dim results = From student In Students _
                       Where arg.GetRemainingCount > 0 Select student.name
     End Sub

For the above query expression, compiler generates the following error:

"error BC36598: Instance of restricted type 'System.ArgIterator' cannot be used in a query expression."

 

Conclusion

Query Comprehension provides a concise and compositional expression of query with the basic query capabilities that includes different pre-defined query operator keyword such as projection, selection, cross-product, grouping and sorting, which enables you to write SQL like queries directly in your VB code with better IntelliSense experience in the IDE. And query Comprehension is supported by other new features such as Type Inference, Extension Method, Lambda Expression, Anonymous Types and Lexical Closure. Lexical Closure is the main purpose of this article, which is not an immediately visible feature to you and also is not intended to be used directly from your code, but rather it is under the hood compile feature. But knowing and understanding what the compiler do behind the scene will help you to write simple, correct and fast query statement directly from your VB.NET code.

 


Author


Binyam Kelile is a software development engineer with the Visual Basic test team at Microsoft.

Show:
© 2014 Microsoft