Chapter 6. Generics

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.

Using Generics

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.

Generic Types

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.

Basic Usage

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.

Figure 6-1

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

Figure 6-2

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

Inheritance

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

Figure 6-3

Figure 6.3. Figure 6-3

Generic Methods

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.

Creating Generics

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.

Generic Types

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.

Classes

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.

Figure 6-4

Figure 6.4. Figure 6-4

Other Generic Class Features

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.

Classes and Inheritance

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.

Structures

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.

Interfaces

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.

Generic Methods

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.

Constraints

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.

Type Constraints

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.

Figure 6-5

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

Class and Structure Constraints

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.

New Constraints

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.

Multiple Constraints

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.

Generics and Late Binding

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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset