LateCType: a strongly-typed latebound conversion operator
In my last post, I discussed how to use Microsoft.VisualBasic.CompilerServices.Conversions.ChangeType to perform latebound conversions. In this post, I will take its usage a step further and show how to define a strongly-typed but latebound conversion operator.
Conversion operators in VB (generally) perform conversions on compile-time types and give strongly-typed results. ChangeType , on the other hand, performs conversions on types determined at run-time and returns Object. If we blend these two concepts, we can achieve some surprisingly useful behavior:
Shared Function LateCType(Of T)(ByVal Value As Object) As T
Return CType(Conversions.ChangeType(Value, GetType(T)), T)
End Function
LateCType is a generic function that converts the parameter Value into an object of type T. The conversion is performed with ChangeType using T and Value’s type at run-time to resolve the conversion path. ChangeType returns an Object, so we must cast down to T to obtain a strongly-typed result. Because LateCType is generic, this one function can be used for any strongly-typed T that we require. For example:
Dim i As Integer = LateCType(Of Integer)(o)
Dim c As Class1 = LateCType(Of Class1)(o)
Dim q As List(Of String) = LateCType(Of List(Of String))(o)
In other words, this is a strongly-typed latebound conversion operator.
Why is this useful? Say I have a class ‘Base’ with a derived class ‘Derived’ and that both convert to String:
Class Base
Shared Narrowing Operator CType(ByVal Value As Base) As String
Return "mars"
End Operator
End Class
Class Derived : Inherits Base
Shared Narrowing Operator CType(ByVal Value As Derived) As String
Return "jupiter"
End Operator
End Class
Now if I have a variable of type Base and convert it to String using CType, class Base’s conversion operator will be called:
Dim b As Base = New Derived
Debug.WriteLine(CType(b, String))
This code snippet will print “mars” even though b contains an instance of Derived. Why? Because CType uses the source and target types as they are at compile time: Base and String. Therefore, it picks Base’s conversion operator. Using LateCType however, the results are different:
Debug.WriteLine(LateCType(Of String)(b))
This code snippet will print “jupiter” because b contains an instance of Derived at run-time, and it is this type which used by LateCType to resolve and perform the conversion. Note that the result is a strongly-typed String expression even though the conversion is latebound.
You might think the example above is not particularly compelling. I agree. So let’s now consider using LateCType in a different context. Generic type parameters seem to have very similar properties to LateCType: strongly-typed yet resolved at run-time. It’s no surprise then that LateCType is particularly well suited for doing conversions on generic type parameters.
For example, here’s a generic class which represents a space-delimited pair of values:
Public Class DelimitedPair(Of L, R)
Private m_Left As L
Private m_Right As R
Private Const Delimiter As Char = " "c
Shared Narrowing Operator CType(ByVal Value As String) As DelimitedPair(Of L, R)
'TODO: Error checking.
Dim Components As String() = Value.Split(Delimiter)
Dim Result As New DelimitedPair(Of L, R)
Result.m_Left = LateCType(Of L)(Components(0))
Result.m_Right = LateCType(Of R)(Components(1))
Return Result
End Operator
Shared Narrowing Operator CType(ByVal Value As DelimitedPair(Of L, R)) As String
'TODO: Error checking.
Return _
LateCType(Of String)(Value.m_Left) & _
Delimiter & _
LateCType(Of String)(Value.m_Right)
End Operator
Public Overrides Function ToString() As String
Return CStr(Me)
End Function
End Class
Notice the use of LateCType to cast to and from the type parameters L and R. Depending on the types used to instantiate this class, the appropriate conversion operators will be called (assuming of course that those types convert to/from String). For example:
Dim x As DelimitedPair(Of Double, Date)
x = CType("23.766 1/1/2005", DelimitedPair(Of Double, Date))
Debug.Print(CStr(x))
This outputs:
23.766 2005/01/01
So going back to the dataset example from the previous post, we can build columns with types such as DelimitedPair where the user-input will be validated and converted automatically even though the underlying types are quite complex:
Module Example1
Sub Main()
'First create a dataset to work with.
Dim Patients As New DataSet("Patients")
Dim ContactInfo As DataTable = Patients.Tables.Add("ContactInfo")
ContactInfo.Columns.Add("Age", GetType(Integer))
ContactInfo.Columns.Add("Weight", GetType(SqlTypes.SqlDouble))
ContactInfo.Columns.Add( _
"LastPayment", _
GetType(DelimitedPair(Of Date, Decimal)))
'Next, call a generalized function to add a row to the dataset.
AddRow(ContactInfo, "29", "63.4", "8/3/2005 1076.43")
'Lastly, print out the results.
For Each Value As Object In ContactInfo.Rows(0).ItemArray
Debug.WriteLine(Value.ToString & " : " & Value.GetType.ToString)
Next
End Sub
...
Running this code (see previous post) will give the result:
29 : System.Int32
63.4 : System.Data.SqlTypes.SqlDouble
2005/08/03 1076.43 : Example2+DelimitedPair`2[System.DateTime,System.Decimal]
Pretty cool. So the choice is up to us, the programmers, to use either early-bound or late-bound conversions where late-bound conversions seem particularly well suited for use on generic type parameters. Again, there is a performance penalty for doing late-bound conversions and you wouldn’t want to run them in a tight-loop. For user-driven input however, no problem.
August 4th, 2005 at 7:08 pm
late bound conversions with generics !