One of the things developers often need to do is create new types for their programs. Early attempts at type creation led to user-defined types, or the use of the VB Structure
statement. Another approach is to use classes and objects to create new types. Ever since the release of the .NET Framework 2.0, another approach is to use generics.
Generics refers to the technology built into the .NET Framework 3.5 (and the .NET Framework versions 2.0 and 3.0) that enables you to define a code template and then declare variables using that template. The template defines the operations that the new type can perform; and when you declare a variable based on the template, you are creating a new type. The benefit of generics over structures or objects is that a generic template makes it easier for your new types to be strongly typed. Generics also make it easier to reuse the template code in different scenarios.
The primary motivation for adding generics to .NET was to enable the creation of strongly typed collection types. Because generic collection types are strongly typed, they are significantly faster than the previous inheritance-based collection model. Anywhere you presently use collection classes in your code, you should consider revising that code to use generic collection types instead.
Visual Basic 2008 allows not only the use of preexisting generics, but also the creation of your own generic templates. Because the technology to support generics was created primarily to build collection classes, it naturally follows that you might create a generic collection anytime you would otherwise build a normal collection class. More specifically, anytime you find yourself using the Object
data type, you should instead consider using generics.
This chapter begins with a brief discussion of the use of generics, followed by a walk-through of the syntax for defining your own generic templates.
There are many examples of generic templates in the .NET 3.5 Base Class Library (BCL). Many of them can be found in the System.Collections.Generic
namespace, but others are scattered through the BCL as appropriate. Many of the examples focus on generic collection types, but this is only because it is here that the performance gains due to generics are most notable. In other cases, generics are used less for performance gains than for the strong typing benefits they provide. As noted earlier, anytime you use a collection data type, you should consider using the generic equivalent instead.
A generic is often written as something like List(Of T)
. The type (or class) name in this case is List
. The letter T
is a placeholder, much like a parameter. It indicates where you must provide a specific type value to customize the generic. For instance, you might declare a variable using the List(Of T)
generic:
Dim data As New List(Of Date)
In this case, you are specifying that the type parameter, T
, is a Date
. By providing this type, you are specifying that the list will only contain values of type Date
. To make this clearer, let's contrast the new List(Of T)
collection with the older ArrayList
type.
When you work with an ArrayList
, you are working with a type of collection that can store many types of values at the same time:
Dim data As New ArrayList() data.Add("Hello") data.Add(5) data.Add(New Customer())
This ArrayList
is loosely typed, internally always storing the values as type Object
. This is very flexible, but relatively slow because it is late bound. Of course, it offers the advantage of being able to store any data type, with the disadvantage that you have no control over what is actually stored in the collection.
The List(Of T)
generic collection is quite different. It is not a type at all; it is just a template. A type is not created until you declare a variable using the template:
Dim data As New Generic.List(Of Integer) data.Add(5) data.Add(New Customer()) ' throws an exception data.Add("Hello") ' throws an exception
When you declare a variable using the generic, you must provide the type of value that the new collection will hold. The result is that a new type is created — in this case, a collection that can only hold Integer
values.
The important thing here is that this new collection type is strongly typed for Integer
values. Not only does its external interface (its Item
and Add
methods, for instance) require Integer
values, but its internal storage mechanism only works with type Integer
. This means that it is not late bound like ArrayList
, but rather is early bound. The net result is much higher performance, along with all the type-safety benefits of being strongly typed.
Generics are useful because they typically offer a higher performance option compared to traditional classes. In some cases, they can also save you from writing code, as generic templates can provide code reuse where traditional classes cannot. Finally, generics can sometimes provide better type safety compared to traditional classes, as a generic adapts to the specific type you require, whereas classes often must resort to working with a more general type such as Object
.
Generics come in two forms: generic types and generic methods. For instance, List(Of T)
is a generic type in that it is a template that defines a complete type or class. In contrast, some otherwise normal classes have single methods that are just method templates and that assume a specific type when they are called. We will look at both scenarios.
Now that you have a basic understanding of generics and how they compare to regular types, let's get into some more detail. To do this, you will make use of some other generic types provided in the .NET Framework. A generic type is a template that defines a complete class, structure, or interface. When you want to use such a generic, you declare a variable using the generic type, providing the real type (or types) to be used in creating the actual type of your variable.
To begin, create a new Windows Application project named "Generics." On Form1
add a Button
control (named btnDictionary
) and a TextBox
control (named txtDisplay
). Set the TextBox
control's Multiline
property to True
and anchor it to take up most of the form. You can also set the text of the button to Dictionary
. The result should look something like what is shown in Figure 6-1.
First, consider the Dictionary(Of K, T)
generic. This is much like the List(Of T)
discussed earlier, but this generic requires that you define the types of both the key data and the values to be stored. When you declare a variable as Dictionary(Of K, T)
, the new Dictionary
type that is created only accepts keys of the one type and values of the other.
Add the following code in the click
event handler for btnDictionary
:
Public Class Form1 Private Sub btnDictionary_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnDictionary.Click txtDisplay.Clear()
Dim data As New Generic.Dictionary(Of Integer, String) data.Add(5, "Bill") data.Add(15, "George") For Each item As KeyValuePair(Of Integer, String) In data txtDisplay.AppendText("Data: " & item.Key & ", " & item.Value) txtDisplay.AppendText(Environment.NewLine) Next txtDisplay.AppendText(Environment.NewLine) End Sub End Class
As you type, watch the IntelliSense information on the Add
method. Notice how the key and value parameters are strongly typed based on the specific types provided in the declaration of the data
variable. In the same code, you can create another type of Dictionary
:
Public Class Form1 Private Sub btnDictionary_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnDictionary.Click txtDisplay.Clear() Dim data As New Generic.Dictionary(Of Integer, String) Dim info As New Generic.Dictionary(Of Guid, Date) data.Add(5, "Bill") data.Add(15, "George") For Each item As KeyValuePair(Of Integer, String) In data txtDisplay.AppendText("Data: " & item.Key & ", " & item.Value) txtDisplay.AppendText(Environment.NewLine) Next txtDisplay.AppendText(Environment.NewLine) info.Add(Guid.NewGuid, Now) For Each item As KeyValuePair(Of Guid, Date) In info txtDisplay.AppendText("Info: " & item.Key.ToString & _ ", " & item.Value) txtDisplay.AppendText(Environment.NewLine) Next txtDisplay.AppendText(Environment.NewLine) End Sub End Class
This code contains two completely different types. Both have the behaviors of a Dictionary
, but they are not interchangeable because they have been created as different types.
Generic types may also be used as parameters and return types. For instance, add the following method to Form1
:
Private Function LoadData() As Generic.Dictionary(Of Integer, String) Dim data As New Generic.Dictionary(Of Integer, String) data.Add(5, "Bill") data.Add(15, "George") Return data End Function
To call this method from the btnDictionary_Click
method, add this code:
Private Sub btnDictionary_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnDictionary.Click txtDisplay.Clear() Dim data As New Generic.Dictionary(Of Integer, String) Dim info As New Generic.Dictionary(Of Guid, Date) data.Add(5, "Bill") data.Add(15, "George") For Each item As KeyValuePair(Of Integer, String) In data txtDisplay.AppendText("Data: " & item.Key & ", " & item.Value) txtDisplay.AppendText(Environment.NewLine) Next txtDisplay.AppendText(Environment.NewLine) info.Add(Guid.NewGuid, Now) For Each item As KeyValuePair(Of Guid, Date) In info txtDisplay.AppendText("Info: " & item.Key.ToString & _ ", " & item.Value) txtDisplay.AppendText(Environment.NewLine) Next txtDisplay.AppendText(Environment.NewLine) Dim results As Generic.Dictionary(Of Integer, String) results = LoadData() For Each item As KeyValuePair(Of Integer, String) In results txtDisplay.AppendText("Results: " & item.Key & ", " & item.Value) txtDisplay.AppendText(Environment.NewLine) Next txtDisplay.AppendText(Environment.NewLine) End Sub
The results of running this code are shown in Figure 6-2.
This works because both the return type of the function and the type of the data variable are exactly the same. Not only are they both Generic.Dictionary
derivatives, they have exactly the same types in the declaration.
The same is true for parameters:
Private Sub DoWork(ByVal values As Generic.Dictionary(Of Integer, String)) ' do work here End Sub
Again, the parameter type is not only defined by the generic type, but also by the specific type values used to initialize the generic template.
It is possible to inherit from a generic type as you define a new class. For instance, the .NET BCL defines the System.ComponentModel.BindingList(Of T)
generic type. This type is used to create collections that can support data binding. You can use this as a base class to create your own strongly typed, data-bindable collection. Add new classes named Customer
and CustomerList
to the project with the following code:
Public Class Customer Private mName As String Public Property Name() As String Get Return mName End Get Set(ByVal value As String) mName = value End Set End Property End Class
Public Class CustomerList Inherits System.ComponentModel.BindingList(Of Customer) Private Sub CustomerList_AddingNew(ByVal sender As Object, _ ByVal e As System.ComponentModel.AddingNewEventArgs) Handles Me.AddingNew Dim cust As New Customer() cust.Name = "<new>" e.NewObject = cust End Sub End Class
When you inherit from BindingList(Of T)
, you must provide a specific type — in this case, Customer
. This means that your new CustomerList
class extends and can customize BindingList(Of Customer)
. Here you are providing a default value for the Name
property of any new Customer
object added to the collection.
When you inherit from a generic type, you can employ all the normal concepts of inheritance, including overloading and overriding methods, extending the class by adding new methods, handling events, and so forth.
To see this in action, add a new Button
control named btnCustomer
to Form1
and add a new form named CustomerForm
to the project. Add a DataGridView
control to CustomerForm
and dock it by selecting the Fill in the parent container option.
Behind the btnCustomer
handler, add the following code:
CustomerForm.ShowDialog()
Then add the following code behind CustomerForm
:
Dim list As New CustomerList() Private Sub CustomerForm_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load DataGridView1.DataSource = list End Sub
This code creates an instance of CustomerList
and makes it the DataSource
for the DataGridView
control. When you run the program and click the button to open the CustomerForm
, notice that the grid contains a newly added Customer
object. As you interact with the grid, new Customer
objects are automatically added, with a default name of <new>
. An example is shown in Figure 6-3.
All this functionality of adding new objects and setting the default Name
value occurs because CustomerList
inherits from BindingList(Of Customer)
.
A generic method is a single method that is called not only with conventional parameters, but also with type information that defines the method. Generic methods are far less common than generic types. Due to the extra syntax required to call a generic method, they are also less readable than a normal method.
A generic method may exist in any class or module; it does not need to be contained within a generic type. The primary benefit of a generic method is avoiding the use of CType
or DirectCast
to convert parameters or return values between different types.
It is important to realize that the type conversion still occurs; generics merely provide an alternative mechanism to use instead of CType
or DirectCast
.
Without generics, code often uses the Object
type. Add the following method to Form1
:
Public Function AreEqual(ByVal a As Object, ByVal b As Object) As Boolean Return a.Equals(b) End Function
The problem with this code is that a
and b
could be anything. There is no restriction here, nothing to ensure that they are even the same type. An alternative is to use generics. Add the following method to Form1
:
Public Function AreEqual(Of T)(ByVal a As T, ByVal b As T) As Boolean Return a.Equals(b) End Function
Now a
and b
are forced to be the same type, and that type is specified when the method is invoked.
Add a new Button
named btnEqual
to Form1
with the following code in its click
event:
Dim result As Boolean ' use normal method result = AreEqual(1, 2) result = AreEqual("one", "two")
result = AreEqual(1, "two") ' use generic method result = AreEqual(Of Integer)(1, 2) result = AreEqual(Of String)("one", "two") 'result = AreEqual(Of Integer)(1, "two")
However, why not just declare the method as a Boolean
? This code will probably cause some confusion. The first three method calls are invoking the normal AreEqual
method. Notice that there is no problem asking the method to compare an Integer
and a String
.
The second set of calls looks very odd. At first glance, they look like nonsense to many people. This is because invoking a generic method means providing two sets of parameters to the method, rather than the normal one set of parameters.
The first set of parameters defines the type or types required to define the method. This is much like the list of types you must provide when declaring a variable using a generic class. In this case, you're specifying that the AreEqual
method will be operating on parameters of type Integer
.
The second set of parameters are the conventional parameters that you'd normally supply to a method. What is special in this case is that the types of the parameters are being defined by the first set of parameters. In other words, in the first call, the type is specified to be Integer
, so 1
and 2
are valid parameters. In the second call, the type is String
, so "one"
and "two"
are valid. Notice that the third line is commented out. This is because 1
and "two"
aren't the same type, so the compiler won't compile that line of code.
Now that you have a good idea how to use preexisting generics in your code, let's take a look at how you can create generic templates. The primary reason to create a generic template instead of a class is to gain strong typing of your variables. Anytime you find yourself using the Object
data type, or a base class from which multiple types inherit, you may want to consider using generics. By using generics, you can avoid the use of CType
or DirectCast
, thereby simplifying your code. If you can avoid using the Object
data type, you will typically improve the performance of your code.
As discussed earlier, there are generic types and generic methods. A generic type is basically a class or structure that assumes specific type characteristics when a variable is declared using the generic. A generic method is a single method that assumes specific type characteristics, even though the method might be in an otherwise very conventional class, structure, or module.
Recall that a generic type is a class, structure, or interface template. You can create such templates yourself to provide better performance, strong typing, and code reuse to the consumers of your types.
A generic class template is created in the same way that you create a normal class, with the exception that you require the consumer of your class to provide you with one or more types for use in your code. In other words, as the author of a generic template, you have access to the type parameters provided by the user of your generic.
For example, add a new class to the project named SingleLinkedList
:
Public Class SingleLinkedList(Of T) End Class
In the declaration of the type, you specify the type parameters that will be required:
Public Class SingleLinkedList(Of T)
In this case, you are requiring just one type parameter. The name, T
, can be any valid variable name. In other words, you could declare the type like this:
Public Class SingleLinkedList(Of ValueType)
Make this change to the code in your project.
By convention (carried over from C++ templates), the variable names for type parameters are single uppercase letters. This is somewhat cryptic, and you may want to use a more descriptive convention for variable naming.
Whether you use the cryptic standard convention or more readable parameter names, the parameter is defined on the class definition. Within the class itself, you then use the type parameter anywhere that you would normally use a type (such as String
or Integer
).
To create a linked list, you need to define a Node
class. This will be a nested class:
Public Class SingleLinkedList(Of ValueType) #Region " Node class " Private Class Node Private mValue As ValueType Private mNext As Node Public ReadOnly Property Value() As ValueType Get Return mValue End Get End Property Public Property NextNode() As Node Get Return mNext End Get Set(ByVal value As Node) mNext = value End Set End Property Public Sub New(ByVal value As ValueType, ByVal nextNode As Node) mValue = value
mNext = nextNode End Sub End Class #End Region End Class
Notice how the mValue
variable is declared as ValueType
. This means that the actual type of mValue
depends on the type supplied when an instance of SingleLinkedList
is created.
Because ValueType
is a type parameter on the class, you can use ValueType
as a type anywhere in the code. As you write the class, you cannot tell what type ValueType
will be. That information is provided by the user of your generic class. Later, when someone declares a variable using your generic type, that person will specify the type, like this:
Dim list As New SingleLinkedList(Of Double)
At this point, a specific instance of your generic class is created, and all cases of ValueType
within your code are replaced by the VB compiler with Double
. Essentially, this means that for this specific instance of SingleLinkedList
, the mValue
declaration ends up as follows:
Private mValue As Double
Of course, you never get to see this code, as it is dynamically generated by the .NET Framework's JIT compiler at runtime based on your generic template code.
The same is true for methods within the template. Your example contains a constructor method, which accepts a parameter of type ValueType
. Remember that ValueType
will be replaced by a specific type when a variable is declared using your generic.
So, what type is ValueType
when you are writing the template itself? Because it can conceivably be any type when the template is used, ValueType
is treated like the Object
type as you create the generic template. This severely restricts what you can do with variables or parameters of ValueType
within your generic code.
The mValue
variable is of ValueType
, which means it is basically of type Object
for the purposes of your template code. Therefore, you can do assignments (as you do in the constructor code), and you can call any methods that are on the System.Object
type:
Equals()
GetHashCode()
GetType()
ReferenceEquals()
ToString()
No operations beyond these basics are available by default. Later in the chapter, you will learn about the concept of constraints, which enables you to restrict the types that can be specified for a type parameter Constraints have the added benefit that they expand the operations you can perform on variables or parameters defined based on the type parameter.
However, this capability is enough to complete the SingleLinkedList
class. Add the following code to the class after the End Class
from the Node
class:
Private mHead As Node Default Public ReadOnly Property Item(ByVal index As Integer) As ValueType Get Dim current As Node = mHead For index = 1 To index current = current.NextNode If current Is Nothing Then Throw New Exception("Item not found in list") End If Next Return current.Value End Get End Property Public Sub Add(ByVal value As ValueType) mHead = New Node(value, mHead) End Sub Public Sub Remove(ByVal value As ValueType) Dim current As Node = mHead Dim previous As Node = Nothing While current IsNot Nothing If current.Value.Equals(value) Then If previous Is Nothing Then ' this was the head of the list mHead = current.NextNode Else previous.NextNode = current.NextNode End If Exit Sub End If previous = current current = current.NextNode End While ' You got to the end without finding the item. Throw New Exception("Item not found in list") End Sub
Public ReadOnly Property Count() As Integer Get Dim result As Integer = 0 Dim current As Node = mHead While current IsNot Nothing result += 1 current = current.NextNode End While Return result End Get End Property
Notice that the Item
property and the Add
and Remove
methods all use ValueType
as either return types or parameter types. More important, note the use of the Equals
method in the Remove
method:
If current.Value.Equals(value) Then
The reason why this compiles is that Equals
is defined on System.Object
and is therefore universally available. This code could not use the =
operator because that is not universally available.
To try out the SingleLinkedList
class, add a button to Form1
named btnList
and add the following code to Form1
:
Private Sub btnList_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnList.Click Dim list As New SingleLinkedList(Of String) list.Add("Bill") list.Add("Tuija") list.Add("Sofia") list.Add("Henri") list.Add("Kalle") txtDisplay.Clear() txtDisplay.AppendText("Count: " & list.Count) txtDisplay.AppendText(Environment.NewLine) For index As Integer = 0 To list.Count - 1 txtDisplay.AppendText("Item: " & list.Item(index)) txtDisplay.AppendText(Environment.NewLine) Next End Sub
When you run the code, you will see a display similar to Figure 6-4.
Earlier in the chapter, you used the Dictionary
generic, which specifies multiple type parameters. To declare a class with multiple type parameters, you use syntax like the following:
Public Class MyCoolType(Of T, V) Private mValue As T Private mData As V Public Sub New(ByVal value As T, ByVal data As V) mValue = value mData = data End Sub End Class
In addition, it is possible to use regular types in combination with type parameters, as shown here:
Public Class MyCoolType(Of T, V) Private mValue As T Private mData As V Private mActual As Double Public Sub New(ByVal value As T, ByVal data As V, ByVal actual As Double) mValue = value mData = data mActual = actual End Sub End Class
Other than the fact that variables or parameters of types T
or V
must be treated as type System.Object
, you can write virtually any code you choose. The code in a generic class is really no different from the code you'd write in a normal class.
This includes all the object-oriented capabilities of classes, including inheritance, overloading, overriding, events, methods, properties, and so forth. However, there are some limitations on overloading. In particular, when overloading methods with a type parameter, the compiler does not know what that specific type might be at runtime. Thus, you can only overload methods in ways in which the type parameter (which could be any type) does not lead to ambiguity.
For instance, adding these two methods to MyCoolType
before the .NET Framework 3.5 would have resulted in a compiler error:
Public Sub DoWork(ByVal data As Integer) ' do work here End Sub Public Sub DoWork(ByVal data As V) ' do work here End Sub
Now this is possible due to the support for implicitly typed variables. During compilation in .NET 3.5, the compiler figures out what the data type of V
should be and makes the appropriate changes to compile V
as what is utilized in your code. This was not the case prior to .NET 3.5. Before this version of the .NET Framework, this kind of code would have resulted in a compiler error. It wasn't legal because the compiler didn't know whether V
would be an Integer
at runtime. If V
were to end up defined as an Integer
, then you'd have two identical method signatures in the same class.
Not only can you create basic generic class templates, you can also combine the concept with inheritance. This can be as basic as having a generic template inherit from an existing class:
Public Class MyControls(Of T) Inherits Control End Class
In this case, the MyControls
generic class inherits from the Windows Forms Control
class, thus gaining all the behaviors and interface elements of a Control
.
Alternately, a conventional class can inherit from a generic template. Suppose that you have a simple generic template:
Public Class GenericBase(Of T) End Class
It is quite practical to inherit from this generic class as you create other classes:
Public Class Subclass Inherits GenericBase(Of Integer) End Class
Notice how the Inherits
statement not only references GenericBase
, but also provides a specific type for the type parameter of the generic type. Anytime you use a generic type, you must provide values for the type parameters, and this is no exception. This means that your new Subclass
actually inherits from a specific instance of GenericBase
, where T
is of type Integer
.
Finally, you can also have generic classes inherit from other generic classes. For instance, you can create a generic class that inherits from the GenericBase
class:
Public Class GenericSubclass(Of T) Inherits GenericBase(Of Integer) End Class
As with the previous example, this new class inherits from an instance of GenericBase
, where T
is of type Integer
.
Things can get far more interesting. It turns out that you can use type parameters to specify the types for other type parameters. For instance, you could alter GenericSubclass
like this:
Public Class GenericSubclass(Of V) Inherits GenericBase(Of V) End Class
Notice that you're specifying that the type parameter for GenericBase
is V
—which is the type provided by the caller when it declares a variable using GenericSubclass
. Therefore, if a caller does
Dim obj As GenericSubclass(Of String)
then V
is of type String
, meaning that GenericSubclass
is inheriting from an instance of GenericBase
, where its T
parameter is also of type String
. The type flows through from the subclass into the base class. If that is not complex enough, consider the following class definition:
Public Class GenericSubclass(Of V) Inherits GenericBase(Of GenericSubclass(Of V)) End Class
In this case, the GenericSubclass
is inheriting from GenericBase
, where the T
type in GenericBase
is actually a specific instance of the GenericSubclass
type. A caller can create such an instance as follows:
Dim obj As GenericSubclass(Of Date)
In this case, the GenericSubclass
type has a V
of type Date
. It also inherits from GenericBase
, which has a T
of type GenericSubclass(Of Date)
.
Such complex relationships are typically not useful, but it is important to recognize how types flow through generic templates, especially when inheritance is involved.
You can also define generic Structure
types. Structures are discussed in Chapter 1. The basic rules and concepts are the same as for defining generic classes, as shown here:
Public Structure MyCoolStructure(Of T) Public Value As T End Structure
As with generic classes, the type parameter or parameters represent real types that are provided by the user of the Structure in actual code. Thus, anywhere you see a T
in the structure; it will be replaced by a real type such as String
or Integer
.
Code can use the structure in a manner similar to how a generic class is used:
Dim data As MyCoolStructure(Of Guid)
When the variable is declared, an instance of the Structure
is created based on the type parameter provided. In this example, an instance of MyCoolStructure
that holds Guid
objects has been created.
Finally, you can define generic interface types. Generic interfaces are a bit different from generic classes or structures because they are implemented by other types when they are used. You can create a generic interface using the same syntax used for classes and structures:
Public Interface ICoolInterface(Of T) Sub DoWork(ByVal data As T) Function GetAnswer() As T End Interface
Then the interface can be used within another type. For instance, you might implement the interface in a class:
Public Class ARegularClass Implements ICoolInterface(Of String) Public Sub DoWork(ByVal data As String) _ Implements ICoolInterface(Of String).DoWork End Sub Public Function GetAnswer() As String _ Implements ICoolInterface(Of String).GetAnswer End Function End Class
Notice that you provide a real type for the type parameter in the Implements
statement and Implements
clauses on each method. In each case, you are specifying a specific instance of the ICoolInterface
interface — one that deals with the String
data type.
As with classes and structures, an interface can be declared with multiple type parameters. Those type parameter values can be used in place of any normal type (such as String
or Date
) in any Sub, Function, Property
, or Event
declaration.
You have already seen examples of methods declared using type parameters such as T
or V
. While these are examples of generic methods, they have been contained within a broader generic type such as a class, structure, or interface.
It is also possible to create generic methods within otherwise normal classes, structures, interfaces, or modules. In this case, the type parameter is not specified on the class, structure, or interface, but rather is specified directly on the method itself.
For instance, you can declare a generic method to compare equality like this:
Public Module Comparisons Public Function AreEqual(Of T)(ByVal a As T, ByVal b As T) As Boolean Return a.Equals(b) End Function End Module
In this case, the AreEqual
method is contained within a module, though it could just as easily be contained in a class or structure. Notice that the method accepts two sets of parameters. The first set of parameters is the type parameter — in this example, just T
. The second set of parameters consists of the normal parameters that a method would accept. In this example, the normal parameters have their types defined by the type parameter, T
.
As with generic classes, it is important to remember that the type parameter is treated as a System.Object
type as you write the code in your generic method. This severely restricts what you can do with parameters or variables declared using the type parameters. Specifically, you can perform assignment and call the various methods common to all System.Object
variables.
In a moment you will look at constraints, which enable you to restrict the types that can be assigned to the type parameters and expand the operations that can be performed on parameters and variables of those types.
As with generic types, a generic method can accept multiple type parameters:
Public Class Comparisons Public Function AreEqual(Of T, R)(ByVal a As Integer, ByVal b As T) As R ' implement code here End Function End Class
In this example, the method is contained within a class, rather than a module. Notice that it accepts two type parameters, T
and R
. The return type is set to type R
, whereas the second parameter is of type T
. Also, look at the first parameter, which is a conventional type. This illustrates how you can mix conventional types and generic type parameters in the method parameter list and return types, and by extension within the body of the method code.
At this point, you have learned how to create and use generic types and methods, but there have been serious limitations on what you can do when creating generic type or method templates thus far. This is because the compiler treats any type parameters as the type System.Object
within your template code. The result is that you can assign the values and call the various methods common to all System.Object
instances, but you can do nothing else. In many cases, this is too restrictive to be useful.
Constraints offer a solution and at the same time provide a control mechanism. Constraints enable you to specify rules about the types that can be used at runtime to replace a type parameter. Using constraints, you can ensure that a type parameter is a Class
or a Structure
, or that it implements a certain interface or inherits from a certain base class.
Not only do constraints enable you to restrict the types available for use, but they also give the VB compiler valuable information. For example, if the compiler knows that a type parameter must always implement a given interface, then the compiler will allow you to call the methods on that interface within your template code.
The most common type of constraint is a type constraint. A type constraint restricts a type parameter to be a subclass of a specific class or to implement a specific interface. This idea can be used to enhance the SingleLinkedList
to sort items as they are added. First, change the declaration of the class itself to add the IComparable
constraint:
Public Class SingleLinkedList(Of ValueType As IComparable)
With this change, ValueType
is not only guaranteed to be equivalent to System.Object
, it is also guaranteed to have all the methods defined on the IComparable
interface.
This means that within the Add
method you can make use of any methods in the IComparable
interface (as well as those from System.Object
). The result is that you can safely call the CompareTo
method defined on the IComparable
interface, because the compiler knows that any variable of type ValueType
will implement IComparable
:
Public Sub Add(ByVal value As ValueType) If mHead Is Nothing Then ' List was empty, just store the value. mHead = New Node(value, mHead) Else Dim current As Node = mHead Dim previous As Node = Nothing While current IsNot Nothing If current.Value.CompareTo(value) > 0 Then If previous Is Nothing Then ' this was the head of the list mHead = New Node(value, mHead) Else ' insert the node between previous and current
previous.NextNode = New Node(value, current) End If Exit Sub End If previous = current current = current.NextNode End While ' you're at the end of the list, so add to end previous.NextNode = New Node(value, Nothing) End If End Sub
Note the call to the CompareTo
method:
If current.Value.CompareTo(value) > 0 Then
This is possible because of the IComparable
constraint on ValueType
. If you run the code now, the items should be displayed in sorted order, as shown in Figure 6-5.
Not only can you constrain a type parameter to implement an interface, but you can also constrain it to be a specific type (class) or subclass of that type. For example, you could implement a generic method that works on any Windows Forms control:
Public Shared Sub ChangeControl(Of C As Control)(ByVal control As C) control.Anchor = AnchorStyles.Top Or AnchorStyles.Left End Sub
The type parameter, C
, is constrained to be of type Control
. This restricts calling code to only specify this parameter as Control
or a subclass of Control
such as TextBox
.
Then the parameter to the method is specified to be of type C
, which means that this method will work against any Control
or subclass of Control
. Because of the constraint, the compiler now knows that the variable will always be some type of Control
object, so it allows you to use any methods, properties, or events exposed by the Control
class as you write your code.
Finally, it is possible to constrain a type parameter to be of a specific generic type:
Public Class ListClass(Of T, V As Generic.List(Of T)) End Class
The preceding code specifies that the V
type must be a List(Of T)
, whatever type T
might be. A caller can use your class like this:
Dim list As ListClass(Of Integer, Generic.List(Of Integer))
Earlier in the chapter, in the discussion of how inheritance and generics interact, you saw that things can get quite complex. The same is true when you constrain type parameters based on generic types.
Another form of constraint enables you to be more general. Rather than enforce the requirement for a specific interface or class, you can specify that a type parameter must be either a reference type or a value type.
To specify that the type parameter must be a reference type, you use the Class
constraint:
Public Class ReferenceOnly(Of T As Class) End Class
This ensures that the type specified for T
must be the type of an object. Any attempt to use a value type, such as Integer
or Structure
, results in a compiler error.
Likewise, you can specify that the type parameter must be a value type such as Integer
or a Structure
by using the Structure
constraint:
Public Class ValueOnly(Of T As Structure) End Class
In this case, the type specified for T
must be a value type. Any attempt to use a reference type such as String
, an interface, or a class results in a compiler error.
Sometimes you want to write generic code that creates instances of the type specified by a type parameter. In order to know that you can actually create instances of a type, you need to know that the type has a default public constructor. You can determine this using the New
constraint:
Public Class Factories(Of T As New) Public Function CreateT() As T
Return New T End Function End Class
The type parameter, T
, is constrained so that it must have a public default constructor. Any attempt to specify a type for T
that does not have such a constructor will result in a compile error.
Because you know that T
will have a default constructor, you are able to create instances of the type, as shown in the CreateT
method.
In many cases, you will need to specify multiple constraints on the same type parameter. For instance, you might want to require that a type be a reference type and have a public default constructor.
Essentially, you are providing an array of constraints, so you use the same syntax you use to initialize elements of an array:
Public Class Factories(Of T As {New, Class}) Public Function CreateT() As T Return New T End Function End Class
The constraint list can include two or more constraints, enabling you to specify a great deal of information about the types allowed for this type parameter.
Within your generic template code, the compiler is aware of all the constraints applied to your type parameters, so it allows you to use any methods, properties, and events specified by any of the constraints applied to the type.
One of the primary limitations of generics is that variables and parameters declared based on a type parameter are treated as type System.Object
inside your generic template code. While constraints offer a partial solution, expanding the type of those variables based on the constraints, you are still very restricted in what you can do with the variables.
One key example is the use of common operators. There is no constraint you can apply that tells the compiler that a type supports the +
or −
operators. This means that you cannot write generic code like this:
Public Function Add(Of T)(ByVal val1 As T, ByVal val2 As T) As T Return val1 + val2 End Function
This will generate a compiler error because there is no way for the compiler to verify that variables of type T
(whatever that is at runtime) support the +
operator. Because there is no constraint that you can apply to T
to ensure that the +
operator will be valid, there is no direct way to use operators on variables of a generic type.
One alternative is to use Visual Basic's native support for late binding to overcome the limitations shown here. Recall that late binding incurs substantial performance penalties because a lot of work is done dynamically at runtime, rather than by the compiler when you build your project. It is also important to remember the risks that attend late binding — specifically, the fact that the code can fail at runtime in ways that early-bound code cannot. Nonetheless, given those caveats, late binding can be used to solve your immediate problem.
To enable late binding, be sure to put Option Strict Off
at the top of the code file containing your generic template (or set the project property to change Option Strict
project-wide from the project's properties). Then you can rewrite the Add
function as follows:
Public Function Add(Of T)(ByVal value1 As T, ByVal value2 As T) As T Return CObj(value1) + CObj(value2) End Function
By forcing the value1
and value2
variables to be explicitly treated as type Object
, you are telling the compiler that it should use late binding semantics. Combined with the Option Strict Off
setting, the compiler assumes that you know what you are doing and it allows the use of the +
operator even though its validity can't be confirmed.
The compiled code uses dynamic late binding to invoke the +
operator at runtime. If that operator does turn out to be valid for whatever type T
is at runtime, then this code will work great. In contrast, if the operator is not valid, then a runtime exception will be thrown.
Generics enable you to create class, structure, interface, and method templates. These templates gain specific types based on how they are declared or called at runtime. Generics provide you with another code reuse mechanism, along with procedural and object-oriented concepts.
They also enable you to change code that uses parameters or variables of type Object
(or other general types) to use specific data types. This often leads to much better performance and increases the readability of your code.