Defining and Using Generics in Visual Basic 2005
Harish Kantamneni
Visual Basic Team
Microsoft Corporation
Updated October 2005
Summary: Get an overview of the generics feature in Visual Basic 2005 and the basic concepts involved in defining and using generics and their usefulness to the Visual Basic developer. (15 printed pages)
Introduction
Generic Types
Generic Methods
Constraints
Advanced Topics
Conclusion
Generics allow you to provide better compile-time validation when defining and using the types and methods required to handle unrelated types. Examples of such types and methods include the collection type that is required to store data of any kind, and the sort method that is required to sort arrays of various different data types. Generics also help improve performance in some scenarios. Visual Basic 2005 supports both the creation and consumption of generics. This article explains the various concepts associated with defining and using generics, and the scenarios in which they are useful to you, the Visual Basic developer.
Let's start with the normal collection class as shown in the code of Example 1 below. You can store elements of any type in it, but in order to handle elements of any unrelated type, the elements are stored and treated as objects of type System.Object
.
Example 1: Class MyCollection Sub Add(Byval Key As String, Byval Element As Object) ... End Sub ... End Class
There are two problems with using this collection:
- What if you need to use this collection, but want to store only integers? You can still use this collection, but you could accidentally store elements of other types in it and potentially fail later when your code does not expect to find non-integer elements in your collection object.
- There is performance overhead associated with converting Structures, Enums, and primitive types like
Integer
,Date
, and so on to and fromSystem.Object
.
Both of these problems could be avoided by creating your own collection that is strongly typed to allow elements of Integer
type only, as shown in Code Example 2.
Example 2: Class MyIntegerCollection Sub Add(Byval Key As String, Byval Element As Integer) ... End Sub ... End Class
Now that you have a strongly typed collection of integers, what if you need a strongly typed collection that stores only dates? For this, you need to create another collection MyDateCollection
that will be very similar to MyIntegerCollection
, but customized to handle dates. In this manner, if you want compile-time type checking (that is, strong typing) and better performance, you need to define a separate collection type for each element type. This results in code bloat, loss in productivity, and in the long run, a maintenance issue.
A generic collection can help you avoid this code duplication while still providing the benefits of strong typing and better performance. A generic collection is shown in Code Example 3. It is a collection that is parameterized on the element type (ElementType
), and you can pass in the type of the elements that you want to store in this collection. Code Example 3, includes MyCollection(Of Integer)
, MyCollection(Of Date)
, and so on. In this example, ElementType
is said to be the type parameter for the MyCollection
class and can be used as a normal type within this class.
Example 3: Class MyCollection(Of ElementType) Sub Add(Byval key As String, Byval Element As ElementType) 'Note that the type parameter ElementType is being used inside 'the generic type just like any other type ' Dim Element2 As ElementType ... End Sub ... End Class Dim IntegerCol As New MyCollection(Of Integer) Dim DateCol As New MyCollection(Of Date)
Note that MyCollection(Of Integer)
is strongly typed to accept only integers, and similarly MyCollection(Of Date)
to accept only dates. Also, the implementation can deal with the type parameter type (ElementType)
instead of dealing with System.Object
. Additionally, when you use MyCollection(Of Integer)
, the IL (intermediate language) of the implementation dealing with ElementType
is just-in-time compiled at runtime to deal with the Integer
type and thus avoid conversions to System.Object
.
Besides classes, you can also define structures, interfaces, and delegates to be generic. Also, any number of type parameters can be specified for a generic type. Code Example 4 shows a generic collection that can also be strongly typed to accept keys of any type.
Example 4: Class MyCollection(Of KeyType, ElementType) Sub Add(Byval key As KeyType, Byval Element As ElementType) ... End Sub ... End Class
Like collections, sometimes you need to define and use methods that can deal with elements of many different data types. An example of this situation is the Sort method, which can handle elements of different types like Integer, Date, String, and so forth. In order to handle the different unrelated types, the Sort method is defined to take an array of System.Object
and handle the objects as type System.Object
in its implementation too, as shown in Code Example 5.
Example 5: Sub Sort(Byval Arr() As Object) ... End Sub
This has the same problems associated with the non-generic collection, strong typing and performance. You can solve these problems by making the Sort method generic as shown in Code Example 6. Notice that the generic Sort method is parameterized with types that appear before the normal parameters.
Example 6: Sub Sort(Of T)(Byval Arr() As T) ... End Sub
You can now invoke the generic Sort method by passing a type argument to the type parameter T, and then passing in the normal arguments, as shown in Example 7.
Example 7: Dim IntArray() As Integer = {2, 4, 1, 0} Sort(Of Integer)(IntArray) Dim StringArray() As String = {"A", "C", "B"} Sort(Of String)(StringArray)
You can also invoke the generic Sort method without supplying the type argument for type parameter T
and letting the compiler implicitly infer this from the type of the argument passed to the normal parameter Arr
. Code Example 8 shows the different ways in which you can invoke the generic Sort and Compare methods without explicitly specifying the type arguments. Notice the different cases when the compiler can and cannot infer the type arguments.
Example 8: 'No error – the compiler sees that the type of the normal argument is 'Integer() and infers Integer as the type argument for the type parameter ' Sort(IntArray) 'Error – the compiler does not infer any type from the value Nothing ' Sort(Nothing) Function Compare(Of T)(Byval Arg1 As T, Byval Arg2 As T) As Integer End Function 'Valid – the compiler infers the type argument for T as Integer from the 'types of the arguments passed in to the normal parameters ' Compare(20, 40) 'Error – the compiler cannot infer the type argument for T because using 'the types (Integer and String) of the arguments passed in to the normal \ 'parameters, the type argument could be inferred as either Integer (based 'on the first argument) or as String (based on the second argument) and so 'is ambiguous. ' Compare(20, "A")
From earlier examples, we know that type parameters can be used as normal types within the generic type or method on which they are defined. But what members can be accessed and what conversions are valid for objects of the type parameter type? For example, in the implementation of the Sort method shown in Code Example 9, what members can you access from the objects in the array Arr
passed in? Well, the members of System.Object
are accessible regardless of the type argument passed in because all managed objects inherit from System.Object
. In Code Example 9, System.Object.ToString
is accessed from the variable Element
whose type is the type parameter T
.
Example 9: Sub Sort(Of T)(Byval Arr() As T) 'Do the task of sorting ... 'Print the elements in debug #If DEBUG Then For each Element As T In Arr 'Note that members of System.Object can be directly accessed 'from Element which contains an object of type T ' Debug.WriteLine(Element.ToString) Next #End If End Sub
What if you need to compare the elements in the array Arr
in the Sort method? You cannot use the usual comparison operators because T
could be any type depending on the type passed in during invocation, and these operators are not valid on all types. You could potentially cast the elements to the IComparable
interface and then invoke the CompareTo
method on it. However, there is no guarantee that the type passed in to T
will implement IComparable
. You could establish such a guarantee by specifying this as a condition on the type parameter T
(as shown in Code Example 10), thus indicating to the compiler that only types that implement the IComparable
interface should be allowed as an argument to the type parameter T
. Such conditions specified on the type parameters are known as constraints. Code Example 10 adds the interface IComparable
as a constraint to the type parameter T
, so that objects of type T
can then be converted to IComparable
. In fact, you can access the IComparable.CompareTo
method directly from the objects of type T
without even casting them to IComparable
. Code Example 10 shows the access of the IComparable.CompareTo
member with and without the cast to IComparable
.
Example 10: Sub Sort(Of T As IComparable)(Byval Arr() As T) ... 'Accessing the IComparable.CompareTo member after casting theobject 'of type T to IComparable ' CompareResult = CType(Arr(i), IComparable).CompareTo(Arr(j)) 'The IComparable.CompareTo member can be accessed directly from object 'of type T because T has the IComparable interface as a constraint. ' CompareResult = Arr(i).CompareTo(Arr(j)) ... End Sub
Without the IComparable
constraint, the cast to IComparable
could potentially fail at runtime if the type passed in does not implement Icomparable,
as shown in Code Example 11. In fact, without this constraint, the direct access of CompareTo
from T
is a compile error.
Example 11: Sub Sort(Of T)(Byval Arr() As T) ... 'Cast might fail at runtime if the type passed in to T does not 'implement IComparable ' CompareResult = CType(Arr(i), IComparable).CompareTo(Arr(j)) 'Compile Error – The IComparable.CompareTo member cannot be accessed 'from the object of type T because T does not have the IComparable 'constraint ' CompareResult = Arr(i).CompareTo(Arr(j)) ... End Sub
There are two kinds of constraints in Visual Basic—type constraints and New constraints.
You can use type constraints when you want to ensure that all type arguments passed in to a type parameter inherit from or implement that type. Classes and interfaces can be specified as type constraints. You can access members of the constraint class or interface directly from objects of the type parameter type. You can also convert objects of the type parameter type to its constraint type. Code Example 12 constrains the generic MyCollection
type's element type to Control
, thus allowing only Control
or some type that inherits from it (only Controls) as type arguments to the type parameter ElementType
. With this constraint, you can now access the member of Control
from objects of type ElementType
, as shown in Code Example 12.
Example 12: Class MyCollection(Of KeyType, ElementType As System.Winforms.Forms.Control) Sub Add(Byval Key As KeyType, Byval Element As ElementType) 'Notice that the Name property defined in the Control class can be 'accessed from the object Element whose type is the type parameter 'ElementType because ElementType has the class constraint Control. ' Debug.WriteLine("Adding control " & Element.Name) 'Notice that Element whose type is the type parameter ElementType 'can beconverted to Control because ElementType has the class 'constraint Control ' Dim c As System.Winforms.Forms.Control = Element ... End Sub End Class 'Compile Error because the type argument Integer does not inherit from 'Control Dim col1 As MyCollection(Of String, Integer) 'A specialized collection that holds Buttons Dim col2 As MyCollection(Of String, System.Winforms.Forms.Button)
Code Example 13 shows the use of an interface type as a constraint. We now constrain the generic MyCollection
type's key type to IComparable
. This enables the MyCollection
class to provide a Sort
method that can sort on keys. Because the type parameter KeyType
is constrained to IComparable
, you can now access the CompareTo
method defined in IComparable
directly from objects of the KeyType
type (in the Compare
function).
Example 13: Class MyCollection(Of KeyType As IComparable, ElementType As System.Winforms.Forms.Control) Private Function Compare(Byval Key1 As KeyType, Byval Key2 As KeyType) As Integer 'Notice that the CompareTo function defined in the IComparable 'interface can be accessed from object Key1 whose type is the type 'parameter KeyType because KeyType has the interface constraint 'IComparable. ' Return Key1.CompareTo(Key2) End Function Public Function Sort() As ElementType() .... End Function ... End Class
There are times when you want to create instances of the type parameter type. For example, you may want to define a type factory that creates and tracks instances of a particular type. All the more, you may want to make it generic and pass the factory element type as a type argument as shown in Code Example 14 below. So, now you need to create instances of the type parameter type T
. You could use "New T"
to create an instance of the passed in type, but there is no guarantee that the passed in type will have a constructor, and it is not an interface or a MustInherit class. In order to impose such restrictions on the types passed in as arguments to the type parameter T
, you can specify a New constraint on it. A New constraint specified on a type parameter indicates that the type passed in as the type argument to this type parameter should have a public constructor with no parameters and not be declared MustInherit. Essentially, it should be a type of which you can create an instance using its parameter-less constructor. Besides classes that meet these criteria, any structure or enumerated type can be passed in as a type argument to such type parameters.
Code Example 14 defines a generic class Factory
using new instances of the type passed in as the type of argument that can be created. Notice that because the type parameter T
has a New
constraint, objects of the type passed in to the type parameter can be created using the New T
.
Example 14: Class Factory(Of T As New) Function CreateInstance() As T 'Notice that the type parameter type T can be constructed using 'the New keyword because T has a New constraint ' Dim NewInstance As T = New T ... Return NewInstance End Function End Class Dim ExceptionsFactory As New Factory(Of Exception)
You can optionally specify multiple constraints on the same type parameter. In such cases, the types passed in as type arguments must satisfy all the constraints. Because multiple inheritances is not allowed by the CLR, you can specify only one class type constraint for a type parameter. And since multiple interfaces can be implemented or inherited by the same type, you can specify multiple interface constraints on the same type parameter. You can specify only one New constraint on a type parameter. Note that different kinds of constraints can be specified on the same type parameter.
When both a class constraint and an interface constraint are present on a type parameter, then you can access only the class members directly from objects of the type parameter type. To access the interface members, you need to cast the objects to the constraint interface type. When no class constraint is present, but multiple interface constraints are present, you can access the members of all the interfaces from objects of the type parameter type. If any member names are ambiguous because they are present in multiple interfaces, then you can disambiguate them by casting the objects to the appropriate interface and then accessing the member.
Code Example 15 shows type parameters with multiple constraints. Note that you need to use curly braces ({}) when specifying multiple constraints. You could optionally use curly braces when specifying only one constraint too.
Example 15: Interface I1 Sub A() Sub B() End Interface Interface I2 Sub A() Sub C() End Interface Class C1 Sub Z() End Sub End Class Class C2(Of T1 As {C1, I1, New}, T2 As {I1, I2, New}) Sub Test() Dim Obj1 As New T1 'C1.Z can be accessed from the type parameter T1 because 'of the class constraint C1 ' Obj1.Z() 'Error - I1.A cannot be accessed from T1 because although it 'has the interface constraint I1, its class constraint prevents 'the interface constraint members from being accessed directly ' Obj1.A() Dim Obj2 As New T2 'Error - Ambiguous across the interface constraints I1 and I2 ' Obj2.A 'Disambiguate A by casting to interface I1 and then invoking A ' CType(Obj2, I1).A 'I1.B can be accessed from the type parameter T2 because of 'the interface constraint I1 ' Obj2.B 'I2.C can be accessed from the type parameter T2 because of 'the interface constraint I2 ' Obj2.C End Sub End Class
You can use the type parameters of a generic type only within the body of that type, but note that you can use them inside any types nested inside the generic type as well. Code Example 16 defines a generic type MyCollection
with a nested Enumerator
type, and the nested type uses the type parameter KeyType
of the outer type. Because the outer type's type parameters can be used in the nested type, the nested types MyCollection(of String, Integer).Enumerator
and MyCollection(Of String, Double).Enumerator
whose outer types' type arguments (Integer
and Double
) are different, are not equivalent, but rather two distinct types.
Example 16: Class MyCollection(Of KeyType, ElementType) Class Enumerator 'Note that the type parameter KeyType of the outer 'type can be used in its nested type ' Dim key As KeyType End Class ... End Class 'Error – MyCollection(Of String, Integer).Enumerator and MyCollection(Of 'String, Double).Enumerator are two distinct unrelated types because the 'type arguments passed to the outer types are different. ' Dim x As MyCollection(Of String, Integer).Enumerator = _ New MyCollection(Of String, Double).Enumerator
You can also declare nested types as generic types, as shown in Code Example 17.
Example 17: Class MyCollection1 Class Enumerator(Of EnumEleType) End Class End Class Dim x As MyCollection1.Enumerator(Of Integer) Class MyCollection2(Of Keytype, ElementType) Class Enumerator(Of EnumEleType) Dim EnumEle As EnumEleType Dim key As KeyType Dim Element As ElementType End Class End Class Dim y As MyCollection2(Of String, Integer).Enumerator(Of Double)
In Visual Basic 2005, you can overload types on the number of type parameters. Code Example 18 below defines a MyCollection
type overloaded on zero (non-generic overload), one, and two type parameters. When you refer to the name MyCollection
in code, and depending on the number of type arguments supplied, the compiler matches up with the appropriate overloaded type. Code Example 18 shows how you can use these overloaded types in code. Note that if the different overloads are in the same namespace, but defined in separate assemblies, the compiler still treats them as being overloaded.
Example 18: Class MyCollection ... End Class Class MyCollection(Of ElementType) Shared Sub Test(Byval Col As MyCollection(Of ElementType)) End Sub ... End Class Class MyCollection(Of KeyType, ElementType) Shared Sub Print(Byval Col() As MyCollection(Of KeyType, ElementType)) End Sub ... End Class Dim x As MyCollection 'refers to the type with 0 type parameters Dim y As MyCollection(Of Integer) 'refers to the type with 1 type parameter Dim z As MyCollection(Of Date, Double) 'refers to the type with 2 type parameters Call MyCollection(of Integer).Print(Nothing) 'refers to the type with 1 type parameter
You can also overload methods on the number of type parameters. You can optionally combine this kind of overloading with the method overloading on normal parameters that is permitted in previous versions of Visual Basic. Code Example 19 shows two methods overloaded on one and two type parameters.
Example 19: Class MyComparer Shared Function Compare(Of T)(Arg1 As T, Arg2 As T) As Integer End Function Shared Function Compare(Of T1, T2)(Arg1 As T1, Arg2 As T2) AsInteger End Function End Class Call MyComparer.Compare(Of Integer)(10, 20) Call MyComparer.Compare(Of Integer, Date)(10, #1/1/2004#)
Suppose you want to define a PrintControl
method. There are two ways you could do this within Visual Basic 2005.
- You can define a non-generic method that takes a parameter of type
Control
as shown in Example 20 below. - You can define a generic method as shown in Example 21 below.
Which choice is better, the generic or the non-generic method? Either one of them can take any controls like Button
, TextBox
, and so on that inherit from Control
as their argument . In this case, the generic method does not provide any additional gain despite the additional complexity involved in defining it. So, the non-generic PrintControl
method would be the better option here. In this case, you could use the common inheritance relation and define a non-generic method because the set of types you want to allow is identical to the set of types that satisfy this inheritance relation.
Example 20:
Sub PrintControl(Byval c As Control)
...
End Sub
Example 21:
Sub PrintControl(Of T As Control)(Byval c As T)
...
End Sub
But suppose you want to print only controls that supported an IPrint
interface. You could then define PrintControl
in three possible ways.
- You could define a non-generic method as previously shown in Code Example 20, but this method could be passed in controls that do not implement
IPrint
, and such violations would not be detected at compile time. This would instead result in runtime errors when attempting to cast the controls toIPrint
in the implementation of the method. - Another option would be to define a non-generic method that takes a parameter of type
IPrint
as shown in Code Example 22 below. However, this method could be passed in objects that implementIPrint
, but are not controls. The third option is to define a generic method as shown Code Example 23. This can only be invoked with controls that implementIPrint
, so all bad invocations are caught at compile time.
Example 22: Sub PrintControl(Byval c As IPrint) ... End Sub Example 23: Sub PrintControl(Of T As {Control, IPrint})(Byval c As T) ... End Sub
In this case, the generic method is a better choice because there is no common polymorphic type that would help any of the non-generic methods provide the same compile-time validation as the generic method.
Generics can be used to define common functionality for unrelated types, while retaining type safety and performance. Commonly used types like Collection and HashTable, and commonly used functions like Sort can now be used by the Visual Basic developer in a strongly typed manner, thus enabling better compile-time error detection and improved performance. Additionally, the strong typing provides an even better IntelliSense experience in the IDE. Type argument inference during method invocation makes the use of generic functions easy, and use of the generic overloads of commonly used functions like System.Array.Sort
are transparent to the developer. This benefits both new code as well as older code upon recompilation, even without any modifications.
Harish Kantamneni is a developer on the Visual Basic compiler team. .