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 datatype, 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, which simplifies your code. If you are able to avoid using the Object datatype, you’ll 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 totally 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’re 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.

Tip 

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 (as discussed in Chapter 3):

 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 can’t 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’re 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()

  • GetHashValue()

  • GetType()

  • ToString()

No operations beyond these basics are available by default. Later in the chapter, you’ll 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 either as 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 isn’t 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("Rocky")   list.Add("Mary")   list.Add("Erin")   list.Add("Edward")   list.Add("Juan")   list.Remove("Erin")   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’ll see a display similar to Figure 7-4.

image from book
Figure 7-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 doesn’t know what that specific type might be at runtime. Thus, you can only overload methods in ways in which the type parameter (that could be any type) doesn’t lead to ambiguity.

For instance, adding these two methods to MyCoolType will result 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

This isn’t legal because the compiler can’t know whether V will be Integer at runtime. If V were to end up defined as Integer, then you’d have two identical method signatures in the same class. Likewise, the following is not legal:

 Public Sub DoWork(ByVal value As T)   ' do work here End Sub Public Sub DoWork(ByVal data As V)   ' do work here End Sub

Again, there’s no way for the compiler to be sure that T and V will represent different types at runtime. However, you can declare overloaded methods like this:

 Public Sub DoWork(ByVal data As Integer)   ' do work here End Sub Public Sub DoWork(ByVal value As T, ByVal data As V)   ' do work here End Sub

This works because there’s no possible ambiguity between the two method signatures. Regardless of what types T and V end up as, there’s no way the two DoWork() methods can have the same signature.

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’s not complex enough, consider the following class definition:

 Public Class GenericSubclass(Of V)   Inherits GenericBase(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 2. 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 will be 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)   Public Sub DoWork(ByVal data As T)   Public 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’re specifying a specific instance of the ICoolInterface interface - one that deals with the String datatype.

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’ve already seen examples of methods declared using type parameters such as T or V. While these are examples of generic methods, they’ve 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 isn’t 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 parameters, 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 four methods common to all System.Object variables.

In a moment you’ll 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’ve 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 four 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 let you 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 end 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 7-5.

image from book
Figure 7-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’ll 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 doesn’t 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’ll 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’re 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, letting you 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’s no constraint you can apply that tells the compiler that a type supports the + or operators. This means that you can’t 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’s no way for the compiler to verify that variables of type T (whatever that is at runtime) support the + operator. Because there’s no constraint that you can apply to T to ensure that the + operator will be valid, there’s 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 can’t. 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 projectwide). Then you can rewrite the Add() function as follows:

 Public Function Add(Of T)(ByVal val1 As T, ByVal val2 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’re 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’re 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.




Professional VB 2005 with. NET 3. 0
Professional VB 2005 with .NET 3.0 (Programmer to Programmer)
ISBN: 0470124709
EAN: 2147483647
Year: 2004
Pages: 267

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net