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, a structure, or an 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, except 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 named SingleLinkedList to the project (code file: SingleLinkedList.vb):

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.


Note
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 that lives within your public class:

Public Class SingleLinkedList(Of ValueType)
#Region " Node class "
  Private Class Node
    Private mValue As ValueType
    Public ReadOnly Property Value() As ValueType
        Get
            Return mValue
        End Get
    End Property
    Public Property NextNode() As Node
       
    Public Sub New(ByVal value As ValueType, ByVal newNode As Node)
        mValue = value
        NextNode = newNode
    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 Visual Basic 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 after the End Class statement that closes 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
    '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 because 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 the following, which can be called from the ButtonTest_Click method:

  Private Sub CustomList()
        Dim list As New SingleLinkedList(Of String)
        list.Add("Nikita")
        list.Add("Elena")
        list.Add("Benajmin")
        list.Add("William")
        list.Add("Abigail")
        list.Add("Johnathan")
        TextBoxOutput.Clear()
        TextBoxOutput.AppendText("Count: " & list.Count)
        TextBoxOutput.AppendText(Environment.NewLine)
        For index As Integer = 0 To list.Count - 1
            TextBoxOutput.AppendText("Item: " & list.Item(index))
            TextBoxOutput.AppendText(Environment.NewLine)
        Next
  End Sub

When you run the code, you will see a display similar to Figure 7.5.

Figure 7.5 Output when running sample code

7.5

Other Generic Class Features

Earlier in this chapter, you used the Dictionary generic, which specifies multiple type parameters. To declare a class with multiple type parameters, you use syntax such as the following (code file: MyCoolType.vb):

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 follows:

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 the following 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, the compiler figures out what the data type of V should be. Next it replaces V with that type, which allows your code to compile correctly. 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 declaring a variable of type GenericSubclass. Therefore, if a caller uses a declaration that creates an object as a GenericSubclass(Of String) then V is of type String. This means that the GenericSubclass is now inheriting from an instance of GenericBase, where its T parameter is also of type String. The point being that the type flows through from the subclass into the base class. If that is not complex enough, for those who just want a feel for how twisted this logic can become, 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 based on the declared instance of the GenericSubclass type. A caller can create such an instance with the simple declaration which 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; in fact, they are often counterproductive, making code difficult to follow and debug. The point was that it is important to recognize how types flow through generic templates, especially when inheritance is involved.

Structures

You can also define generic Structure types. 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, a structure, or an 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 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 a 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 assignments 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 Visual Basic 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 kind 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. Create a copy of the class called ComparableLinkedList, changing 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. Update the original Add method with the following implementation (code file: ComparableLinkedList.vb):

  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. Run the project and test this modified code. The items should be displayed in sorted order, as shown in Figure 7.6.

Figure 7.6 Sorted output shown when sample code runs

7.6

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 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 add Option Strict Off at the top of the code file containing your generic template (or set the project property to change Option Strict projectwide 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.

Covariance and Contravariance

As part of Visual Studio 2010, the concepts of covariance and contravariance were brought forward into generics. The basic ideas are related to concepts associated with polymorphism. In short, prior to Visual Studio 2010, if you attempted to take, for example, an instance of a generic that inherits from the base class BindingList and assign that instance to an instance of its base class, you would get an error. The ability to take a specialized or subclass and do a polymorphic assignment to its parent or base class describes covariance.

This topic can get complex, so before moving on to discuss contravariance, let's provide a very simple example of covariance in code. The following declares two classes, Parent and ChildClass, and shows covariance in action (code file: CoVariance.vb):

Public Class Parent(Of T)
       
End Class
       
Public Class ChildClass(Of T)
    Inherits Parent(Of T)
       
End Class
       
Public Class CoVariance
    Public Sub MainMethod()
        Dim cc As New ChildClass(Of String)
        Dim dad As Parent(Of String)
        'Show me the covariance
        dad = cc
    End Sub
End Class

You'll note that ChildClass inherits from Parent. The snippet continues with a method extracted from a calling application. It's called MainMethod and you see that the code creates an instance of ChildClass and declares an instance of Parent. Next it looks to assign the instance cc of ChildClass to the instance dad of type Parent. It is this assignment which illustrates an example of covariance. There are, of course, dozens of different specializations that you could consider, but this provides the basis for all of those examples.

Note, if instead of declaring dad as being a Parent (Of String), the code had declared dad as a Parent (Of Integer), then the assignment of cc to dad would fail because dad would no longer be the correct Parent type. It is important to remember that the type assigned as part of the instantiation of a generic directly impacts the underlying class type of that generic's instance.

Contravariance refers to the ability to pass a derived type when a base type is called for. The reason these features are spoken of in a single topic is that they are both specializations of the variance concept. The difference is mainly an understanding that in the case of contravariance you are passing an instance of ChildClass when a Parent instance was expected. Unfortunately contravariance could be called contraintuitive. You are going to create a base method, and .NET will support its used by derived classes. To illustrate this concept, the following code creates two new classes (they are not generic classes), and then has another code snippet for a method that uses these new classes with generic methods to illustrate contravariance (code file: ContraVariance.vb):

Public Class Base
       
End Class
       
Public Class Derived
    Inherits Base
       
End Class
       
Public Class ContraVariance
       
    Private baseMethod As Action(Of Base) = Sub(param As Base)
                                                'Do something.
                                            End Sub
    Private derivedMethod As Action(Of Derived) = baseMethod
       
    Public Sub MainMethod()
        ' Show the contra-syntax
        Dim d As Derived = New Derived()
        derivedMethod(d)
        baseMethod(d)
    End Sub
       
End Class

As shown in the preceding example, you can have a method that expects an input parameter of type Base as its input parameter. In the past, this method would not accept a call with a parameter of type Derived, but with contravariance the method call will now accept a parameter of type Derived because this derived class will, by definition, support the same interface as the base class, just with additional capabilities that can be ignored. As a result, although at first glance it feels backward, you are in fact able to pass a generic that implements a derived class to a method which is expecting a generic that is defined using a base class.

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

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