title image

journal.nullschool.net


Archive for August 4th, 2005

LateCType: a strongly-typed latebound conversion operator

Thursday, August 4th, 2005

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.