An Abstract Specification for Visual Basic Late Binding
In an earlier post, I talk about the implementation details of late bound expressions. That discussion considers the interface between the VB compiler and Late Binder but lacks an overall discussion of late binding: what it is, and how it works. What I present here is an abstract specification for VB late binding.
To generalize an earlier statement: late binding is all about figuring out which members to use while the program runs. Once the compiler decides that a particular expression requires late bound evaluation, it changes the expression into a helper call into the Late Binder (located in Microsoft.VisualBasic.dll). It is then up to the Late Binder to evaluate the expression during run time. This raises two important questions: Which expressions become late bound? And what does the Late Binder do at run time to evaluate these expressions?
Late Bound Expressions
Only certain kinds of Visual Basic expressions can become late bound. The Visual Basic Language Specification offers a definition:
11.3 Late-Binding ExpressionsWhen the target of a member access expression or index expression is of type Object, the processing of the expression may be deferred until run time. Deferring processing this way is called late binding. Late binding allows Object variables to be used in a typeless way, where all resolution of members is based on the actual run-time type of the value in the variable. If strict semantics are specified by the compilation environment or by Option Strict, late binding causes a compile-time error. Non-public members are ignored when doing late-binding, including for the purposes of overload resolution. Note that, unlike the early-bound case, invoking or accessing a Shared member late bound will cause the invocation target to be evaluated at run time. Note If the expression is an invocation expression for a member defined on System.Object, late binding will not take place.
|
source: Microsoftâ Visual Basicâ .NET Language Specification, Version 7.1, p. 153
From the spec's definition, when the target of a member access expression or index expression is of type Object, the expression may become late bound. In other words, using dot "." or parentheses "()" on Object causes late binding (with one exception: using "." to access a member defined on System.Object remains early bound). In fact, there are five distinct categories of late bound expressions:
Late Bound Category |
Purpose |
Late Call |
calling a method |
Late Index Get |
fetching a value using an index |
Late Get |
fetching the value of a member |
Late Index Set |
setting a value using an index |
Late Set |
setting the value of a member |
Each category represents a usage of either " . " or " ( ) " on Object, and each has a distinct evaluation behavior. The earlier post addressed one of these categories, Late Call, but just scratches the surface as the following scenarios demonstrate:
Sub Run(o As Object, ...)
'Late Call
Call o.x() 'Scenario 1: Note the Call keyword is optional
Call o.x(a) 'Scenario 2: arguments may be supplied
'Late Index Get
v = o(a) 'Scenario 3
'Late Get
v = o.x 'Scenario 4
v = o.x(a) 'Scenario 5: arguments may be supplied
'Late Index Set
o(a) = v 'Scenario 6
'Late Set
o.x = v 'Scenario 7
o.x(a) = v 'Scenario 8: arguments may be supplied
End Sub
Evaluation of Late Bound Expressions
Now that we know what kinds of expressions become late bound, we can discuss how they get evaluated at run time. The evaluation of any expression is a two step process: first decide what operation to do and then perform the operation. In the early binding model, the VB compiler makes the decisions and .NET performs the IL-encoded operations. In the late binding model both steps occur at run time, with the Late Binder making the decisions and .NET Reflection performing the operations. The decision is made by applying VB language rules to the expression. These rules depend on the category of the late bound expression and include member lookup, method overload resolution, shadowing and overriding semantics, etc. Once the rules have been applied and the decision made, the resulting operation is performed using Reflection. As currently designed, there are five types of Reflection operations used by the Late Binder:
System.Reflection.MethodInfo.Invoke -- perform a function call
System.Array.GetValue -- fetch a value from an array
System.Array.SetValue -- store a value into an array
System.Reflection.FieldInfo.GetValue -- fetch the value of a field
System.Reflection.FieldInfo.SetValue -- store a value into a field
All late bound expressions eventually turn into a sequence of one or more of these Reflection operations. For example, consider a Late Get expression accessing a member "Position":
Sub Main()
Dim o As Object = ...
Console.WriteLine(o.Position())
End Sub
Because the expression belongs to the Late Get category, the Late Binder knows that "Position" must refer to a method, a property or a field. Say an object of type Knight is contained in o. There are at least three declarations of the type Knight that could make the expression o.Position() valid:
'First possibility
Class Knight
Public Position As Integer
End Class
'Second possibility
Class Knight
Public Function Position() As Integer
End Function
End Class
'Third possibility
Class Knight
Public ReadOnly Property Position() As Integer
Get
End Get
End Property
End Class
Also, Position cannot refer to a nested type or a method with parameters (since no arguments have been supplied at the call site). These declarations of type Knight would make the expression o.Position() not valid:
'Fourth possibility
Class Knight
Structure Position
Public Value As Integer
End Structure
End Class
'Fifth possibility
Class Knight
Public Sub Position(ByRef Value As Integer)
End Sub
End Class
Upon deciding that Position is a valid member for a Late Get expression, the Late Binder invokes the appropriate Reflection operation. If Position refers to a field, then FieldInfo.GetValue will be called. If it refers to a method, MethodInfo.Invoke will be called instead. If it is not valid, as would be the case for a nested type, an exception is thrown.
An Abstract Description of Late Bound Expression Evaluation
It's useful to consider that each late bound category is implemented as a unique helper function in the Late Binder. From here, I will explain the rules and operations used by each of these helper functions. Included below are several tables, one per late bound category. In the left column are the VB late binding scenarios indicated above. In the right column are the operations that result after applying the language rules. This column can also include late binding scenarios. Their inclusion means that the resulting operations are identical to that of the scenario indicated.
The tables make use of the following definitions:
o is an expression typed as Object.
x is an identifier
a is an argument list
v is an expression, either l-value or r-value
d is a default property
Also, I will use the following shorthand to represent the Reflection operations:
INVOKE o.x(a)
call MethodInfo.Invoke(o, a) using the MethodInfo object for method x.
ARRAYGET o[a]
call CType(o, System.Array).GetValue(a)
ARRAYSET o[a] = v
call CType(o, System.Array).SetValue(v, a)
FIELDGET o.x
call FieldInfo.GetValue(o) using the FieldInfo for field x.
FIELDSET o.x = v
call FieldInfo.SetValue(o, v) using the FieldInfo for field x.
Finally, I give a very rough pseudo-code implementation of each helper function. The pseudo-code makes use of a function called PerformMemberResolution(). Consider this the member lookup and resolution process that applies shadowing, overloading, overriding and other various language semantics as described in-depth in the Visual Basic Language Specification. It is dependent upon the instance o, the member name "x" and the supplied arguments a (if any).
Late Call
A simple method call.
Late Bound Scenario |
Possible Operations |
Comments |
Call o.x() 'Scenario 1 |
INVOKE o.x() |
when x is a method |
|
INVOKE o.get_x() |
when x is a property |
Call o.x(a) 'Scenario 2 |
INVOKE o.x(a) |
when x is a method |
|
INVOKE o.get_x(a) |
when x is a property |
If x is a sub or function then invoke x. If x is a property, invoke the get_x accessor function. The call may or may not have arguments.
Helper function pseudo-code:
Function LateCall(Instance|o, MemberName|"x", Arguments|a)
Dim Result As Reflection.MemberInfo = _
PerformMemberResolution(Instance, MemberName, Arguments)
If IsMethod(Result) Then
Return CType(Result, MethodInfo).Invoke(Instance, Arguments)
ElseIf IsProperty(Result) Then
Return CType(Result, PropertyInfo).GetGetMethod.Invoke(Instance, Arguments)
End If
Throw New Exception("Unable to resolve late call expression")
End Function
Late Index Get
Fetching the value from an Object like an array.
VB Scenario |
Possible Operations |
Comments |
v = o(a) 'Scenario 3 |
ARRAYGET o[a] |
where o is an array |
|
INVOKE o.get_d(a) |
where o is a type with a default property |
This scenario indexes into o to load a value. This scenario works if the object in o is either an array or a type with a default property. If o is an array, then use the supplied indices a to load the value. If o is a type with a default property d, invoke the get_d accessor function.
Note: This scenario requires arguments. If no arguments are supplied, this would be the simple assignment v = o (not a late bound expression).
Helper function pseudo-code:
Function LateIndexGet(Instance|o, Arguments|a)
If IsArray(Instance) Then
Return CType(Instance, System.Array).GetValue(Arguments)
ElseIf HasDefaultProperty(Instance) Then
GetDefaultProperty(Instance).GetGetMethod.Invoke(Instance, Arguments)
End If
Throw New Exception("Unable to resolve late index get expression")
End Function
Late Get
Doing member access on an Object to fetch a value.
VB Scenario |
Possible Operations |
Comments |
v = o.x 'Scenario 4 |
FIELDGET o.x |
where x is a field |
|
call o.x() 'Scenario 1 |
|
v = o.x(a) 'Scenario 5 |
w(a) 'Scenario 3 |
w := FIELDGET o.x |
|
w(a) 'Scenario 3 |
w := o.x 'Scenario 4 |
|
call o.x(a) 'Scenario 2 |
|
This looks like Late Call but the requirement for a resulting value changes the kinds of members to which "x" can refer. Note that this is also different from Late Index Get since we're using the member access operator " . " instead of the indexing operator " ( ) ".
With no arguments supplied, x can be either a field or a method. When x is a method, the resulting operation is the Late Call expression call o.x() (look up Scenario 1 in the Late Call table to see the possible operations).
With arguments supplied, x can be a method but also a field under special circumstances. When x is a field, fetch the value of field x and then use that in a new Late Index Get expression w(a) . In other words, do a Late Index Get into the result of a Late Get. Similarly, if x is a method with no parameters, do a Late Call on that method to get the result w and then use it in Late Index Get w(a) . Here's an example with the two steps explicitly expressed:
'First possibility
Class Knight
Public Moves As Integer()
End Class
'Second possibility
Class Knight
Public Function Moves() As Integer()
End Function
End Class
Sub Main()
Dim o As Object = New Knight
Dim w As Object = o.Moves 'First late bound expression
Dim v As Integer = w(10) 'Second late bound expression where w contains an array
End Sub
Lastly, if x is a method that takes arguments, then treat it as a Late Call.
Helper function pseudo-code:
Function LateGet(Instance|o, MemberName|"x", Arguments|a)
Dim Result As Reflection.MemberInfo = _
PerformMemberResolution(Instance, MemberName, Arguments)
If Arguments.Count = 0 Then
If IsField(Result) Then
Return CType(Result, FieldInfo).GetValue(Instance)
ElseIf IsMethod(Result) Then
Return LateCall(Instance, MemberName, NoArguments)
End If
Else
If IsField(Result) Then
Dim w As Object = CType(Result, FieldInfo).GetValue(Instance)
Return LateIndexGet(w, Arguments)
ElseIf IsParameterlessMethod(Result) Then
Dim w As Object = CType(Result, MethodInfo).Invoke(Instance, NoArguments)
Return LateIndexGet(w, Arguments)
ElseIf IsMethod(Result) Then
Return LateCall(Instance, MemberName, Arguments)
End If
End If
Throw New Exception("Unable to resolve late get expression")
End Function
Late Index Set
Storing a value into an Object like an array.
VB Scenario |
Possible Operations |
Comments |
o(a) = v 'Scenario 6 |
ARRAYSET o[a] = v |
where o is an array |
|
INVOKE o.set_d(v) |
where o is a type with a default property |
This scenario indexes into o to store a value. This scenario works if the object in o is either an array or a type with a default property. If o is an array, then use the supplied indices a to store the value. If o is a type with a default property d, invoke the set_d accessor function.
Note: This scenario requires arguments. If no arguments are supplied, this would be the simple assignment o = v (not a late bound expression).
Helper function pseudo-code:
Sub LateIndexSet(Instance|o, Arguments|a, Value|v)
If IsArray(Instance) Then
Instance.SetValue(Value, Arguments)
Return
ElseIf HasDefaultProperty(Instance) Then
GetDefaultProperty(Instance).GetSetMethod.Invoke(Instance, Arguments + Value)
Return
End If
Throw New Exception("Unable to resolve late index set expression")
End Sub
Late Set
Doing member access on an Object to set a value.
VB Scenario |
Possible Operations |
Comments |
o.x = v 'Scenario 7 |
FIELDSET o.x = v |
where x is a field |
|
INVOKE o.set_x(v) |
|
o.x(a) = v 'Scenario 8 |
w(a) 'Scenario 3 |
w := FIELDGET o.x |
|
INVOKE o.set_x(a, v) |
|
|
w(a) = v 'Scenario 3 |
w := o.x 'Scenario 4 |
Again, the existence of arguments changes the situation. The member x can either be a field, method, or property. If x is a field and no arguments are supplied, perform a field set. If arguments were supplied, fetch the value w in field x and then do a Late Index Set w(a) .
If x is a property, this is a simple invocation of the set_x accessor. If x is a parameterless method, then invoke x to get the resulting value w and perform a Late Index Set w(a) .
Helper function pseudo-code:
Sub LateSet(Instance|o, MemberName|"x", Arguments|a, Value|v)
Dim Result As Reflection.MemberInfo = _
PerformMemberResolution(Instance, MemberName, Arguments)
If Arguments.Count = 0 Then
If IsField(Result) Then
CType(Result, FieldInfo).SetValue(Instance, Value)
Return
ElseIf IsProperty(Result) Then
CType(Result, PropertyInfo).GetSetMethod.Invoke(Instance, Value)
Return
End If
Else
If IsField(Result) Then
Dim w As Object = CType(Result, FieldInfo).GetValue(Instance)
LateIndexSet(w, Arguments, Value)
Return
ElseIf IsProperty(Result) Then
CType(Result, PropertyInfo).GetSetMethod.Invoke(Instance, Arguments + Value)
Return
ElseIf IsParameterlessMethod(Result) Then
Dim w As Object = CType(Result, MethodInfo).Invoke(Instance, NoArguments)
LateIndexSet(w, Arguments, Value)
Return
End If
End If
Throw New Exception("Unable to resolve late set expression")
End Sub
In Closing
We've seen that only some expressions become late bound, and that these expressions are grouped into five major categories. Each category uses a unique set of VB language rules to decide the expression's validity, and that after this decision process is made, a series of Reflection operations are used to perform the expression. Each category can be thought of as a helper function with specific behavior, and these behaviors have been described as a set of scenarios and resulting operations.
A final note: the Visual Basic runtime contains a function CallByName. The CallByName function performs a late bound evaluation on the given object, member name, and arguments. There is also an enum parameter UseCallType of type Microsoft.VisualBasic.CallType. Essentially, this enum parameter allows the programmer to choose the late binding category. CallType.Get, CallType.Set, and CallType.Method correspond to Late Get, Late Set, and Late Call respectively (a fourth value, CallType.Let, is used only for COM interop). In fact, the CallByName function is simply a Select Case statement that uses the CallType argument to choose the appropriate late binding helper function.
Comments
- Anonymous
June 27, 2004
Thank you for some very useful information. - Anonymous
July 01, 2004
Real!It's a very useful article.thx^_^,C# can also use it.