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)

Contents

Introduction
Generic Types
Generic Methods
Constraints
Advanced Topics
Conclusion

Introduction

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.

Generic Types

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:

  1. 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.
  2. There is performance overhead associated with converting Structures, Enums, and primitive types like Integer, Date, and so on to and from System.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

Generic Methods

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

Constraints

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.

Type 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

New Constraints

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)

Multiple Constraints

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

Advanced Topics

Nested Types

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)

Overloading on Type Parameters

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

Generics vs. Polymorphism

Suppose you want to define a PrintControl method. There are two ways you could do this within Visual Basic 2005.

  1. You can define a non-generic method that takes a parameter of type Control as shown in Example 20 below.
  2. 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.

  1. 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 to IPrint in the implementation of the method.
  2. 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 implement IPrint, 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 implement IPrint, 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.

Conclusion

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. .

© Microsoft Corporation. All rights reserved.