Chapter 29. Generics

Classes are often described as cookie cutters for creating objects. You define a class, and then you can use it to make any number of objects that are instances of the class.

Similarly, a generic is like a cookie cutter for creating classes. You define a generic, and then you can use it to create any number of classes that have similar features.

For example, Visual Basic comes with a generic List class. You can use it to make lists of strings, lists of integers, lists of Employee objects, or lists of just about anything else.

This chapter explains generics. It shows how you define generics of your own and how you can use them.

ADVANTAGES OF GENERICS

A generic class takes one or more data types as parameters. An instance of a generic class has those parameters filled in with specific data types such as String, TextBox, or Employee.

For example, you can build a list of OrderItem objects, a hash table containing PurchaseOrders identified by number, or a Queue that contains Customer objects.

Tying generics to specific data types gives them a few advantages over more traditional classes:

  • Strong typing — Methods can take parameters and return values that have the class's instance type. For example, a List(Of String) can hold only string values, and its Item method returns string values. This makes it more difficult to accidentally add the wrong type of object to the collection.

  • IntelliSense — By providing strong typing, a class built from a generic lets Visual Studio provide IntelliSense. If you make a List(Of Employee), Visual Studio knows that the items in the collection are Employee objects, so it can give you appropriate IntelliSense.

  • No boxing — Because the class manipulates objects with a specific data type, Visual Basic doesn't need to convert items to and from the plain Object data type. For example, if a program stores TextBox controls in a normal collection, the program must convert the TextBox controls to and from the Object class when it adds and uses items in the collection. Avoiding these steps makes the code more efficient.

  • Code reuse — You can use a generic class with more than one data type. For example, if you have built a generic PriorityQueue class, you can make a PriorityQueue holding Employee, Customer, Order, or Objection objects. Without generics, you would need to build four separate classes to build strongly typed priority queues for each of these types of objects. Reusing this code makes it easier to write, test, debug, and maintain the code.

The main disadvantage to generics is that they are slightly more complicated and confusing than non-generic classes. If you know that you will only ever need to provide a class that works with a single type, you can simplify things slightly by not using a generic class. If you think you might want to reuse the code later for another data type, it's easier to just build the class generically from the start.

DEFINING GENERICS

Visual Basic allows you to define generic classes, structures, interfaces, procedures, and delegates. The basic syntax is similar, so when you understand how to make generic classes, the others should be fairly easy.

To define a generic class, make a class declaration as usual. After the class name, add a parenthesis, the keyword Of, and a placeholder for a data type. For example, the following code shows the outline of a generic MostRecentList class. Its declaration takes one type that the class internally names ItemType. This is similar to a parameter name that you would give to a subroutine. The class's code can use the name ItemType to refer to the type associated with the instance of the generic class.

Public Class MostRecentList(Of ItemType)
    ...
End Class

For example, suppose that you want to make a list that can act as a most recently used (MRU) file list. It should be able to hold at most four items. New items are added at the top of the list, and the others are bumped down one position with the last item being dropped if the list contains too many items. If you add an existing item to the list, it jumps to the top of the list.

Example program GenericMruList uses the following code to build a generic MostRecentList class:

' A list of at most MaxItems items.
Public Class MostRecentList(Of ItemType)
    ' The Item property.
    Private m_Items As New List(Of ItemType)
    Public Property Item(ByVal index As Integer) As ItemType
        Get
            Return m_Items(index)
        End Get
Set(ByVal value As ItemType)
            m_Items(index) = value
        End Set
    End Property

    ' The MaxItems property.
    Private m_MaxItems As Integer = 4
    Public Property MaxItems() As Integer
        Get
            Return m_MaxItems
        End Get
        Set(ByVal value As Integer)
            m_MaxItems = value

            ' Resize appropriately.
            Do While m_Items.Count > m_MaxItems
                m_Items.RemoveAt(m_Items.Count - 1)
            Loop
        End Set
    End Property

    ' The current number of items.
    Public ReadOnly Property Count() As Integer
        Get
            Return m_Items.Count
        End Get
    End Property

    ' Add an item to the top of the list.
    Public Sub Add(ByVal value As ItemType)
        ' Remove the item if it is present.
        If m_Items.Contains(value) Then m_Items.Remove(value)

        ' Add the item to the top of the list.
        m_Items.Insert(0, value)

        ' Make sure there are at most MaxItems items.
        If m_Items.Count > m_MaxItems Then m_Items.RemoveAt(m_Items.Count - 1)
    End Sub

    ' Remove an item.
    Public Sub Remove(ByVal value As ItemType)
        m_Items.Remove(value)
    End Sub

    ' Remove an item at a specific position.
    Public Sub RemoveAt(ByVal index As Integer)
        m_Items.RemoveAt(index)
    End Sub
End Class
                                                  
DEFINING GENERICS

The Of ItemType clause indicates that the class will take a single type that it internally names ItemType. The class stores its items in a private list named m_Items. It declares this list using the generic list class defined in the System.Collections.Generic namespace, and it indicates that this is a list that will hold ItemType objects. This refers to the ItemType parameter used in the generic MostRecentList class's declaration. If the program makes a MostRecentList of strings, then m_Items is a list of strings.

In this code, the Item property procedures simply let the main program get and set the values in the m_Items list. The MaxItems property lets the program determine the number of items that the list can hold. The property Set routine saves the new size and then resizes the m_Items list appropriately if necessary. The Count property returns the number of items currently in the list. The subroutine Add first removes the new item if it is already in the list. It then adds the new item at the top of the list and removes the last item if the list now contains too many items. The Remove and RemoveAt routines simply call the m_Items list's Remove and RemoveAt methods.

The following code creates a new MostRecentList of strings and then adds some values to it:

Dim the_items As New MostRecentList(Of String)
the_items.Add("Apple")
the_items.Add("Banana")
the_items.Add("Cherry")
the_items.Add("Date")
the_items.Add("Banana")
the_items.Add("Fig")

After this code executes, the list contains the values in the following order: Fig, Banana, Date, Cherry.

Generic Constructors

Generic classes can have constructors just as any other class can. For example, the following constructor initializes the MostRecentList class's MaxItem property:

' Initialize MaxItems for the new list.
Public Sub New(ByVal max_items As Integer)
    MaxItems = max_items
End Sub

To use the constructor, the main program adds normal parameters after the type parameters in the object declaration. The following statement creates a new MostRecentList of strings, passing its constructor the value 4:

Dim the_items As New MostRecentList(Of String)(4)

Multiple Types

If you want the class to work with more than one type, you can add other types to the declaration separated by commas. For example, suppose that you want to create a list of items where each key is associated with a pair of data items. Example program GenericPairDictionary uses the following code to define the generic PairDictionary class. This class acts as a dictionary that associates a key value with a pair of data values. Notice how the class declaration includes three data types named KeyType, DataType1, and DataType2.

' A Dictionary that associates
' a pair of data values with each key.
Public Class PairDictionary(Of KeyType, DataType1, DataType2)
    ' A structure to hold paired data.
    Private Structure DataPair
        Public Data1 As DataType1
        Public Data2 As DataType2
        Public Sub New(ByVal data_value1 As DataType1,
         ByVal data_value2 As DataType2)
            Data1 = data_value1
            Data2 = data_value2
    End Sub
End Structure

    ' A Dictionary to hold the paired data.
    Private m_Dictionary As New Dictionary(Of KeyType, DataPair)

    ' Return the number of data pairs.
    Public ReadOnly roperty Count() As Integer
        Get
            Return m_Dictionary.Count
        End Get
    End Property

    ' Add a key and data pair.
    Public Sub Add(ByVal key As KeyType,
     ByVal data_value1 As DataType1,
     ByVal data_value2 As DataType2)
        m_Dictionary.Add(key, New DataPair(data_value1, data_value2))
    End Sub

    ' Remove all data.
    Public Sub Clear()
        m_Dictionary.Clear()
    End Sub

    ' Return True if the PairDictionary contains this key.
    Public Function ContainsKey(ByVal key As KeyType) As Boolean
        Return m_Dictionary.ContainsKey(key)
    End Function

    ' Return a data pair.
    Public Sub GetItem(ByVal key As KeyType,
     ByRef data_value1 As DataType1,
     ByRef data_value2 As DataType2)
       Dim data_pair As DataPair = m_Dictionary.Item(key)
       data_value1 = data_pair.Data1
       data_value2 = data_pair.Data2
    End Sub
' Set a data pair.
    Public Sub SetItem(ByVal key As KeyType,
     ByVal data_value1 As DataType1,
     ByVal data_value2 As DataType2)
        m_Dictionary.Item(key) = New DataPair(data_value1, data_value2)
    End Sub

    ' Return a collection containing the keys.
    Public ReadOnly Property Keys() As System.Collections.ICollection
        Get
            Return m_Dictionary.Keys()
        End Get
    End Property

    ' Remove a particular entry.
        Public Sub Remove(ByVal key As KeyType) m_Dictionary.Remove(key)
    End Sub
End Class
                                                  
Multiple Types

The PairDictionary class defines its own private DataPair class to hold data pairs. The DataPair class has two public variables of types DataType1 and DataType2. Its only method is a constructor that makes initializing the two variables easier.

After defining the DataPair class, the PairDictionary class declares a generic Dictionary object named m_Dictionary using the key type KeyType and data type DataPair.

PairDictionary provides Count, Add, Clear, ContainsKey, GetItem, SetItem, Keys, and Remove methods. Notice how it delegates these to the m_Dictionary object and how it uses the DataPair class to store values in m_Dictionary.

The following code creates an instance of the generic PairDictionary class that uses integers as keys and strings for both data values. It adds three entries to the PairDictionary and then retrieves and displays the entry with key value 32.

' Create the PairDictionary and add some data.
Dim pair_dictionary As New PairDictionary(Of Integer, String, String)
pair_dictionary.Add(10, "Ann", "Archer")
pair_dictionary.Add(32, "Bill", "Beach")
pair_dictionary.Add(17, "Cynthia", "Campos")

' Print the values for index 32.
Dim value1 As String = ""
Dim value2 As String = ""
pair_dictionary.GetItem(32, value1, value2)
Debug.WriteLine(value1 & ", " & value2)

Constrained Types

To get the most out of your generic classes, you should make them as flexible as possible. Depending on what the class will do, however, you may need to constrain the types used to create instances of the generic.

For example, consider the generic MostRecentList class described earlier in this chapter. It stores at most a certain number of objects in a list. When you add an object to the list, the class first removes the object from the list if it is already present.

That works with simple data types such as integers and strings. However, suppose that you want the list to hold Employee objects. When you add a new Employee object, the list tries to remove the item if it is already present in its m_Items list. However, you are adding a new instance of the Employee class. The object may have the same values as an object that is already in the list, but the list won't know that because the values are stored in two different objects.

What the list needs is a way to compare objects in the list to see if they are equal. It can then look through the list and remove an existing item if it matches the new one.

One way to allow the list to compare items is to guarantee that the items implement the IComparable interface. Then the program can use their CompareTo methods to see if two objects match.

Example program GenericMruList2 uses the following code to make a new version of the MostRecentList class. Instead of calling the m_Items list's Remove method directly, the Add method now calls the class's Remove method. That method loops through the list using each item's CompareTo method to see if the item matches the target item. If there is a match, the program removes the item from the list.

Public Class MostRecentList(Of ItemType As IComparable)
    ...
    ' Add an item to the top of the list.
    Public Sub Add(ByVal value As ItemType)
        ' Remove the item if it is present.
        Remove(value)

        ' Add the item to the top of the list.
        m_Items.Insert(0, value)

        ' Make sure there are at most MaxItems items.
        If m_Items.Count >> m_MaxItems Then m_Items.RemoveAt(m_Items.Count - 1)
    End Sub

    ' Remove an item.
    Public Sub Remove(ByVal value As ItemType)
        ' Find the item.
        For i As Integer = m_Items.Count − 1 To 0 Step −1
            If value.CompareTo(m_Items(i)) = 0 Then
                m_Items.RemoveAt(i)
            End If
        Next i
    End Sub
   ...
End Sub
                                                  
Constrained Types

A type's As clause can specify any number of interfaces and at most one class from which the type must be derived. It can also include the keyword New to indicate that the type used must provide a constructor that takes no parameters. If you include more than one constraint, the constraints should be separated by commas and enclosed in brackets.

The following code defines the StrangeGeneric class that takes three type parameters. The first type must implement the IComparable interface and must provide an empty constructor. The second type has no constraints, and the third type must be a class that inherits from Control.

Public Class StrangeGeneric(Of Type1 As {IComparable, New}, Type2,
 Type3 As Control)
    ...
End Class

The following code declares an instance of the StrangeGeneric class:

Dim my_strange_generic As New StrangeGeneric(Of Integer, Employee, Button)

Constraining a type gives Visual Basic more information about that type, so it lets you use the properties and methods defined by the type. In the previous code, for example, if a variable is of type Type3, then Visual Basic knows that it inherits from the Control class, so you can use Control properties and methods such as Anchor, BackColor, Font, and so forth.

USING GENERICS

The previous sections have already shown a few examples of how to use a generic class. The program declares the class and includes whatever data types are required in parentheses. The following code shows how a program might create a generic list of strings:

Imports System.Collections.Generic
...
Dim names As New List(Of String)

To use a generic class's constructor, add a second set of parentheses and any parameters after the type specifications. The following statement creates an IntStringList object, passing it the types Integer, String, and Employee. It calls the class's constructor, passing it the value 100.

Dim the_employees As New IntStringList(Of Integer, String, Employee)(100)

If the program needs to use only a few generic classes (for example, a single collection of strings), this isn't too bad. If the program needs to use many instances of the class, however, the code becomes cluttered.

For example, suppose that the TreeNode class shown in the following code represents a node in a tree. Its MyData field holds some piece of data and its Children list holds references to child nodes.

Public Class TreeNode(Of DataType)
    Public MyData As DataType
    Public Children As New List(Of TreeNode(Of DataType))

    Public Sub New(ByVal new_data As DataType)
        MyData = new_data
    End Sub
End Class

The following code uses this class to build a small tree of Employee objects:

Dim root As New TreeNode(Of Employee)(New Employee("Annabelle", "Ant"))
Dim child1 As New TreeNode(Of Employee)(New Employee("Bert", "Bear"))
Dim child2 As New TreeNode(Of Employee)(New Employee("Candice", "Cat"))

root.Children.Add(child1)
root.Children.Add(child2)

Example program GenericTree, which is available for download on the book's web site, uses similar code to build a generic Tree(Of DataType) class.

Repeating the nodes' data types in the first three lines makes the code rather cluttered. Two techniques that you can use to make the code a bit simpler are using an imports alias and deriving a new class. Both of these let you create a simpler name for the awkward class name TreeNode(Of Employee).

Imports Aliases

Normally, you use an Imports statement to make it easier to refer to namespaces and the symbols they contain. However, the Imports statement also lets you define an alias for a namespace entity. To use this to make using generics easier, create an Imports statement that refers to the type of generic class you want to use and give it a simple alias.

For example, the following code is in the DataTreeTest namespace. It uses an Imports statement to refer to a TreeNode of Employee. It gives this entity the alias EmployeeNode. Later, the program can use the name EmployeeNode to create a TreeNode of Employee.

Imports EmployeeNode = DataTreeTest.TreeNode(Of DataTreeTest.Employee)
...
Dim root As New EmployeeNode(New Employee("Annabelle", "Ant"))
Dim child1 As New EmployeeNode(New Employee("Bert", "Bear"))
Dim child2 As New EmployeeNode(New Employee("Candice", "Cat"))

root.Children.Add(child1)
root.Children.Add(child2)
...
                                                  
Imports Aliases

Example program GenericTreeImportsAlias demonstrates this approach.

Derived Classes

A second method that simplifies using generics is to derive a class from the generic class. The following code derives the EmployeeNode class from TreeNode(Of Employee). Later, it creates instances of this class to build the tree.

Public Class EmployeeNode
    Inherits TreeNode(Of Employee)
    Public Sub New(ByVal new_data As Employee)
        MyBase.New(new_data)
    End Sub
End Class
...
Dim root As New EmployeeNode(New Employee("Annabelle", "Ant"))
Dim child1 As New EmployeeNode(New Employee("Bert", "Bear"))
Dim child2 As New EmployeeNode(New Employee("Candice", "Cat"))

root.Children.Add(child1)
root.Children.Add(child2)
...
                                                  
Derived Classes

Example program GenericTreeSubclass demonstrates this approach.

If you use this technique, you can also add extra convenience functions to the derived class. For example, the following code shows a new EmployeeNode constructor that creates the Employee object that it holds:

Public Sub New(ByVal first_name As String, ByVal last_name As String)
    MyBase.New(New Employee(first_name, last_name))
End Sub

PREDEFINED GENERIC CLASSES

The System.Collections.Generic namespace defines several generic classes. These are basically collection classes that use generics to work with the data type you specify. See the section "Generics" near the end of Chapter 28, "Collection Classes," for more information and a list of the predefined generic collection classes.

GENERIC METHODS

Generics are usually used to build classes that are not data type-specific such as the generic collection classes. You can also give a class (generic or otherwise) a generic method. Just as a generic class is not tied to a particular data type, the parameters of a generic method are not tied to a specific data type.

The method's declaration includes an Of clause similar to the one used by generic classes, followed by the method's parameter list.

Example program UseSwitcher uses the following code to define a generic Switch subroutine. This subroutine defines the type T and takes two parameters of type T. If this were a function, you could use the type T for its return value if you wanted. Subroutine Switch declares a variable temp of type T and uses it to switch the values of its parameters.

Public Class Switcher
    Public Sub Switch(Of T)(ByRef thing1 As T, ByRef thing2 As T)
        Dim temp As T = thing1
        thing1 = thing2
        thing2 = temp
    End Sub
End Class
                                                  
GENERIC METHODS

The following code uses a Switcher object to switch the values of two Person variables. In the call to the Switch method, Visual Basic uses the first parameter to infer that the type T is Person and then requires the second parameter to have the same type.

Dim person1 As New Person("Anna")
Dim person2 As New Person("Bill")
Dim a_switcher As New Switcher
a_switcher.Switch(person1, person2)

GENERICS AND EXTENSION METHODS

Just as extension methods allow you to add new features to existing classes, they also allow you to add new features to generic classes. For example, suppose you have an application that uses a List(Of Person). This List class is a generic collection class defined in the System.Collections.Generic namespace.

The generic class is not defined in your code so you cannot modify it, but you can add extension methods to it. The following code adds an AddPerson method to List(Of Person) that takes as parameters a first and last name, uses those values to make a Person object, and adds it to the list:

Module PersonListExtensions
    <Extension()>
    Public Sub AddPerson(ByVal person_list As List(Of Person),
     ByVal first_name As String, ByVal last_name As String)
        Dim per As New Person() With _
            {.FirstName = first_name, .LastName = last_name}
        person_list.Add(per)
    End Sub
End Module

This example adds an extension method to a specific instance of a generic class. In this example, the code adds the method to List(Of Person). With a little more work, you can add a generic extension method to a generic class itself instead of adding it to an instance of the class.

Example program GenericNumDistinct uses the following code to add a NumDistinct function to the generic List(Of T) class for any type T. The declaration identifies its generic type T. The first parameter has type List(Of T) so this method extends List(Of T). The function has an Integer return type.

Module ListExtensions
    <Extension()>
    Public Function NumDistinct(Of T)(ByVal the_list As List(Of T)) As Integer
        Return the_list.Distinct().Count()
    End Function
End Module
                                                  
GENERICS AND EXTENSION METHODS

The generic List(Of T) class provides a Distinct method that returns a new list containing the distinct objects in the original list. The NumDistinct function calls that method and returns the new list's Count value.

The following code shows how a program could call this function. It creates a new List(Of String) and gives it some data. It then calls the list's NumDistinct function.

Dim name_list As New List(Of String)
name_list.Add("Llamaar Aarchibald")
name_list.Add("Dudley Eversol")
...

MessageBox.Show("The list contains " & name_list.NumDistinct() &
 " distinct entries")

For more information on extension methods, see the section "Extension Methods" in Chapter 17, "Subroutines and Functions."

SUMMARY

A class abstracts the properties and behaviors of a set of objects to form a template that you can use to make objects that implement those properties and behaviors. After you define the class, you can make many instances of it, and they will all have the features defined by the class.

Generics take abstraction one level higher. A generic class abstracts the features of a set of classes defined for specific data types. It determines the properties and methods that any class in the generic group provides. After you define the generic class, you can easily make classes that work with different data types but that all provide the common set of features defined by the generic.

By defining common functionality, generic classes let you reuse code to perform similar actions for different data types. By allowing you to parameterize the class instances with a data type, they let you build strongly typed classes quickly and easily. That, in turn, lets Visual Basic provide IntelliSense to make programming faster and easier.

Together these benefits — easier code reuse, strong typing, and IntelliSense support — help you write, test, debug, and maintain code more easily.

Up to this point, the book's chapters describe fairly general Visual Basic programming topics such as building forms, using controls, using loops to repeat a series of instructions, and building classes. The chapters in the next part of the book move to a slightly more specific topic: graphics programming. They explain how to draw lines, ellipses, curves, and text. They show how to use different colors, line styles, and brush types. They also explain how to manipulate bitmapped images, print, and generate reports. Though the techniques are not needed in as many situations as are the techniques described in previous chapters, they are still useful under a wide variety of circumstances.

Chapter 30 explains the fundamentals of drawing graphics in Visual Basic. It provides the information you need to start drawing simple shapes and curves. The chapters that follow build on these techniques and use them to carry out more specific graphical tasks.

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

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