Chapter 17. Introducing generics

After completing this chapter, you will be able to:

Chapter 8 shows you how to use the object type to refer to an instance of any class. You can use the object type to store a value of any type, and you can define parameters by using the object type when you need to pass values of any type into a method. A method can also return values of any type by specifying object as the return type. Although this practice is very flexible, it puts the onus on the programmer to remember what sort of data is actually being used. This can lead to run-time errors if the programmer makes a mistake. In this chapter, you will learn about generics, a feature that has been designed to help you prevent this kind of mistake.

The problem with the object type

To understand generics, it is worth looking in detail at the problem for which they are designed to solve.

Suppose that you needed to model a first-in, first-out structure such as a queue. You could create a class such as the following:

class Queue
{
    private const int DEFAULTQUEUESIZE = 100;
    private int[] data;
    private int head = 0, tail = 0;
    private int numElements = 0;
    public Queue()
    {
        this.data = new int[DEFAULTQUEUESIZE];
    }
    public Queue(int size)
    {
        if (size > 0)
        {
            this.data = new int[size];
        }
        else
        {
            throw new ArgumentOutOfRangeException("size", "Must be greater than zero");
        }
    }
    public void Enqueue(int item)
    {
        if (this.numElements == this.data.Length)
        {
            throw new Exception("Queue full");
        }
        this.data[this.head] = item;
        this.head++;
        this.head %= this.data.Length;
        this.numElements++;
    }
    public int Dequeue()
    {
        if (this.numElements == 0)
        {
            throw new Exception("Queue empty");
        }
        int queueItem = this.data[this.tail];
        this.tail++;
        this.tail %= this.data.Length;
        this.numElements--;
        return queueItem;
    }
}

This class uses an array to provide a circular buffer for holding the data. The size of this array is specified by the constructor. An application uses the Enqueue method to add an item to the queue and the Dequeue method to pull an item off the queue. The private head and tail fields keep track of where to insert an item into the array and where to retrieve an item from the array. The numElements field indicates how many items are in the array. The Enqueue and Dequeue methods use these fields to determine where to store or retrieve an item from and perform some rudimentary error checking. An application can create a Queue object and call these methods, as shown in the code example that follows. Notice that the items are dequeued in the same order in which they are enqueued:

Queue queue = new Queue(); // Create a new Queue
queue.Enqueue(100);
queue.Enqueue(-25);
queue.Enqueue(33);
Console.WriteLine("{0}", queue.Dequeue());  // Displays 100
Console.WriteLine("{0}", queue.Dequeue());  // Displays -25
Console.WriteLine("{0}", queue.Dequeue());  // Displays 33

Now, the Queue class works well for queues of ints, but what if you want to create queues of strings, or floats, or even queues of more complex types such as Circle (see Chapter 7), or Horse, or Whale (see Chapter 12)? The problem is that the way in which the Queue class is implemented restricts it to items of type int, and if you try to enqueue a Horse, you will get a compile-time error.

Queue queue = new Queue();
Horse myHorse = new Horse();
queue.Enqueue(myHorse); // Compile-time error: Cannot convert from Horse to int

One way around this restriction is to specify that the array in the Queue class contains items of type object, update the constructors, and modify the Enqueue and Dequeue methods to take an object parameter and return an object, such as in the following:

class Queue
{
    ...
    private object[] data;
    ...
    public Queue()
    {
        this.data = new object[DEFAULTQUEUESIZE];
    }
    public Queue(int size)
    {
        ...
        this.data = new object[size];
        ...
    }
    public void Enqueue(object item)
    {
        ...
    }
    public object Dequeue()
   {
        ...
        object queueItem = this.data[this.tail];
        ...
        return queueItem;
    }
}

Remember that you can use the object type to refer to a value or variable of any type. All reference types automatically inherit (either directly or indirectly) from the System.Object class in the Microsoft .NET Framework (in C#, object is an alias for System.Object). Now, because the Enqueue and Dequeue methods manipulate objects, you can operate on queues of Circles, Horses, Whales, or any of the other classes that you have seen in earlier exercises in this book. However, it is important to notice that you have to cast the value returned by the Dequeue method to the appropriate type because the compiler will not perform the conversion from the object type automatically.

Queue queue = new Queue();
Horse myHorse = new Horse();
queue.Enqueue(myHorse); // Now legal – Horse is an object
...
Horse dequeuedHorse =(Horse)queue.Dequeue(); // Need to cast object back to a Horse

If you don’t cast the returned value, you will get the compiler error “Cannot implicitly convert type ‘object’ to ‘Horse.’” This requirement to perform an explicit cast degenerates much of the flexibility afforded by the object type. Furthermore, it is very easy to write code such as this:

Queue queue = new Queue();
Horse myHorse = new Horse();
queue.Enqueue(myHorse);
...
Circle myCircle = (Circle)queue.Dequeue(); // run-time error

Although this code will compile, it is not valid and throws a System.InvalidCastException exception at run time. The error is caused by trying to store a reference to a Horse in a Circle variable when it is dequeued, and the two types are not compatible. This error is not spotted until run time because the compiler does not have enough information to perform this check at compile time. The real type of the object being dequeued becomes apparent only when the code runs.

Another disadvantage of using the object approach to create generalized classes and methods is that it can consume additional memory and processor time if the runtime needs to convert an object to a value type and back again. Consider the following piece of code that manipulates a queue of int values:

Queue queue = new Queue();
int myInt = 99;
queue.Enqueue(myInt);         // box the int to an object
...
myInt = (int)queue.Dequeue(); // unbox the object to an int

The Queue data type expects the items it holds to be objects, and object is a reference type. Enqueueing a value type, such as an int, requires it to be boxed to convert it to a reference type. Similarly, dequeueing into an int requires the item to be unboxed to convert it back to a value type. See the sections Boxing and Unboxing in Chapter 8 for more details. Although boxing and unboxing happen transparently, they add performance overhead because they involve dynamic memory allocations. This overhead is small for each item, but it adds up when a program creates queues of large numbers of value types.

The generics solution

C# provides generics to remove the need for casting, improve type safety, reduce the amount of boxing required, and make it easier to create generalized classes and methods. Generic classes and methods accept type parameters, which specify the types of objects on which they operate. In C#, you indicate that a class is a generic class by providing a type parameter in angle brackets, like this:

class Queue<T>
{
    ...
}

The T in this example acts as a placeholder for a real type at compile time. When you write code to instantiate a generic Queue, you provide the type that should be substituted for T (Circle, Horse, int, and so on). When you define the fields and methods in the class, you use this same placeholder to indicate the type of these items, like this:

class Queue<T>
{
    ...
    private T[] data; // array is of type 'T' where 'T' is the type parameter
    ...
    public Queue()
    {
        this.data = new T[DEFAULTQUEUESIZE]; // use 'T' as the data type
    }
    public Queue(int size)
    {
        ...
        this.data = new T[size];
        ...
    }
    public void Enqueue(T item)  // use 'T' as the type of the method parameter
    {
        ...
    }
    public T Dequeue() // use 'T' as the type of the return value
    {
        ...
        T queueItem = this.data[this.tail];  // the data in the array is of type 'T'
        ...
        return queueItem;
    }
}

The type parameter T can be any legal C# identifier, although the lone character T is commonly used. It is replaced with the type you specify when you create a Queue object. The following examples create a Queue of ints, and a Queue of Horses:

Queue<int> intQueue = new Queue<int>();
Queue<Horse> horseQueue = new Queue<Horse>();

Additionally, the compiler now has enough information to perform strict type-checking when you build the application. You no longer need to cast data when you call the Dequeue method, and the compiler can trap any type mismatch errors early:

intQueue.Enqueue(99);
int myInt = intQueue.Dequeue();     // no casting necessary
Horse myHorse = intQueue.Dequeue(); // compiler error:
                                    // cannot implicitly convert type 'int' to 'Horse'

You should be aware that this substitution of T for a specified type is not simply a textual replacement mechanism. Instead, the compiler performs a complete semantic substitution so that you can specify any valid type for T. Here are more examples:

struct Person
{
    ...
}
...
Queue<int> intQueue = new Queue<int>();
Queue<Person> personQueue = new Queue<Person>();

The first example creates a queue of integers, whereas the second example creates a queue of Person values. The compiler also generates the versions of the Enqueue and Dequeue methods for each queue. For the intQueue queue, these methods look like this:

public void Enqueue(int item);
public int Dequeue();

For the personQueue queue, these methods look like this:

public void Enqueue(Person item);
public Person Dequeue();

Contrast these definitions with those of the object-based version of the Queue class shown in the preceding section. In the methods derived from the generic class, the item parameter to Enqueue is passed as a value type that does not require boxing. Similarly, the value returned by Dequeue is also a value type that does not need to be unboxed. A similar set of methods is generated for the other two queues.

Note

The Systems.Collections.Generics namespace in the .NET Framework class library provides an implementation to the Queue class that operates in a similar manner to the class just described. This namespace also includes several other collection classes, and they are described in more detail in Chapter 18.

The type parameter does not have to be a simple class or value type. For example, you can create a queue of queues of integers (if you should ever find it necessary), like this:

Queue<Queue<int>> queueQueue = new Queue<Queue<int>>();

A generic class can have multiple type parameters. For example, the generic Dictionary class defined in the System.Collections.Generic namespace in the .NET Framework class library expects two type parameters: one type for keys, and another for the values (this class is described in more detail in Chapter 18).

Note

You can also define generic structures and interfaces by using the same type-parameter syntax as generic classes.

Generics vs. generalized classes

It is important to be aware that a generic class that uses type parameters is different from a generalized class designed to take parameters that can be cast to different types. For example, the object-based version of the Queue class shown earlier is a generalized class. There is a single implementation of this class, and its methods take object parameters and return object types. You can use this class with ints, strings, and many other types, but in each case, you are using instances of the same class and you have to cast the data you are using to and from the object type.

Compare this with the Queue<T> class. Each time you use this class with a type parameter (such as Queue<int> or Queue<Horse>), you cause the compiler to generate an entirely new class that happens to have functionality defined by the generic class. This means that Queue<int> is a completely different type from Queue<Horse>, but they both happen to have the same behavior. You can think of a generic class as one that defines a template that is then used by the compiler to generate new type-specific classes on demand. The type-specific versions of a generic class (Queue<int>, Queue<Horse>, and so on) are referred to as constructed types, and you should treat them as distinctly different types (albeit ones that have a similar set of methods and properties).

Generics and constraints

Occasionally, you will want to ensure that the type parameter used by a generic class identifies a type that provides certain methods. For example, if you are defining a PrintableCollection class, you might want to ensure that all objects stored in the class have a Print method. You can specify this condition by using a constraint.

By using a constraint, you can limit the type parameters of a generic class to those that implement a particular set of interfaces and therefore provide the methods defined by those interfaces. For example, if the IPrintable interface defined the Print method, you could create the PrintableCollection class like this:

public class PrintableCollection<T> where T : IPrintable

When you build this class with a type parameter, the compiler checks to ensure that the type used for T actually implements the IPrintable interface; if it doesn’t, it stops with a compilation error.

Creating a generic class

The System.Collections.Generic namespace in the .NET Framework class library contains a number of generic classes readily available for you. You can also define your own generic classes, which is what you will do in this section. Before you do this, let’s cover a bit of background theory.

The theory of binary trees

In the following exercises, you will define and use a class that represents a binary tree.

A binary tree is a useful data structure that you can use for a variety of operations, including sorting and searching through data very quickly. Volumes have been written on the minutiae of binary trees, but it is not the purpose of this book to cover this topic in detail. Instead, you’ll look at just the pertinent facts. If you are interested in learning more, consult a book such as The Art of Computer Programming, Volume 3: Sorting and Searching, 2nd Edition by Donald E. Knuth (Addison-Wesley Professional, 1998). Despite its age, this is the recognized, seminal work on sort and search algorithms.

A binary tree is a recursive (self-referencing) data structure that can either be empty or contain three elements: a datum, which is typically referred to as the node, and two subtrees, which are themselves binary trees. The two subtrees are conventionally called the left subtree and the right subtree because they are typically depicted to the left and right of the node, respectively. Each left subtree or right subtree is either empty or contains a node and other subtrees. In theory, the whole structure can continue ad infinitum. The following image shows the structure of a small binary tree.

A diagram showing the structure of a binary tree.

The real power of binary trees becomes evident when you use them for sorting data. If you start with an unordered sequence of objects of the same type, you can construct an ordered binary tree and then walk through the tree to visit each node in an ordered sequence. The algorithm for inserting an item I into an ordered binary tree B is shown here:

If the tree, B, is empty
Then
  Construct a new tree B with the new item I as the node, and empty left and
  right subtrees
Else
  Examine the value of the current node, N, of the tree, B
  If the value of N is greater than that of the new item, I
  Then
    If the left subtree of B is empty
    Then
      Construct a new left subtree of B with the item I as the node, and
      empty left and right subtrees
    Else
      Insert I into the left subtree of B
    End If
  Else
    If the right subtree of B is empty
    Then
      Construct a new right subtree of B with the item I as the node, and
      empty left and right subtrees
    Else
      Insert I into the right subtree of B
    End If
  End If
End If

Notice that this algorithm is recursive, calling itself to insert the item into the left or right subtree depending on how the value of the item compares with the current node in the tree.

Note

The definition of the expression greater than depends on the type of data in the item and node. For numeric data, greater than can be a simple arithmetic comparison, and for text data, it can be a string comparison; however, you must give other forms of data their own means of comparing values. You will learn more about this when you implement a binary tree in the upcoming section Building a binary tree class by using generics.

If you start with an empty binary tree and an unordered sequence of objects, you can iterate through the unordered sequence, inserting each object into the binary tree by using this algorithm, resulting in an ordered tree. The next image shows the steps in the process for constructing a tree from a set of five integers.

A diagram showing hoaw to insert data into a binary tree to create a structure of ordered elements.

After you have built an ordered binary tree, you can display its contents in sequence by visiting each node in turn and printing the value found. The algorithm for achieving this task is also recursive:

If the left subtree is not empty
Then
  Display the contents of the left subtree
End If
Display the value of the node
If the right subtree is not empty
Then
  Display the contents of the right subtree
End If

The following image shows the steps in the process for outputting the tree. Notice that the integers are now displayed in ascending order.

A diagram showing how to iterate through the elements in a binary tree to output them in ascending order.

Building a binary tree class by using generics

In the following exercise, you will use generics to define a binary tree class capable of holding almost any type of data. The only restriction is that the data type must provide a means of comparing values between different instances.

The binary tree class is one that you might find useful in many different applications. Therefore, you will implement it as a class library rather than as an application in its own right. You can then reuse this class elsewhere without having to copy the source code and recompile it. A class library is a set of compiled classes (and other types such as structures and delegates) stored in an assembly. An assembly is a file that usually has the .dll suffix. Other projects and applications can make use of the items in a class library by adding a reference to its assembly and then bringing its namespaces into scope by employing using statements. You will do this when you test the binary tree class.

Create the Tree<TItem> class
  1. Start Microsoft Visual Studio 2013 if it is not already running.

  2. On the File menu, point to New, and then click Project.

  3. In the New Project dialog box, in the Templates pane on the left, click Visual C#. In the middle pane, select the Class Library template. In the Name box, type BinaryTree. In the Location box, specify Microsoft PressVisual CSharp Step By StepChapter 17 in your Documents folder, and then click OK.

    Note

    Using the Class Library template, you can create assemblies that can be reused by multiple applications. To use a class in a class library in an application, you must first copy the assembly containing the compiled code for the class library to your computer (if you did not create it yourself) and then add a reference to this assembly.

  4. In Solution Explorer, right-click Class1.cs, click Rename, and then change the name of the file to Tree.cs. Allow Visual Studio to change the name of the class as well as the name of the file when prompted.

  5. In the Code and Text Editor window, change the definition of the Tree class to Tree<TItem>, as shown in bold type in the following code:

    public class Tree<TItem>
    {
    }
  6. In the Code and Text Editor window, modify the definition of the Tree<TItem> class to specify that the type parameter TItem must denote a type that implements the generic IComparable<TItem> interface. The changes are highlighted in bold in the code example that follows.

    The modified definition of the Tree<TItem> class should look like this:

    public class Tree<TItem> where TItem : IComparable<TItem>
    {
    }
  7. Add three public, automatic properties to the Tree<TItem> class: a TItem property called NodeData and two Tree<TItem> properties called LeftTree and RightTree, as shown in the following code example in bold:

    public class Tree<TItem> where TItem : IComparable<TItem>
    {
        public TItem NodeData { get; set; }
        public Tree<TItem> LeftTree { get; set; }
        public Tree<TItem> RightTree { get; set; }
    }
  8. Add a constructor to the Tree<TItem> class that takes a single TItem parameter called nodeValue. In the constructor, set the NodeData property to nodeValue, and initialize the LeftTree and RightTree properties to null, as shown in bold in the following code:

    public class Tree<TItem> where TItem : IComparable<TItem>
    {
       ...
       public Tree(TItem nodeValue)
       {
            this.NodeData = nodeValue;
            this.LeftTree = null;
            this.RightTree = null;
       }
    }

    Note

    Notice that the name of the constructor does not include the type parameter; it is called Tree, not Tree<TItem>.

  9. Add a public method called Insert to the Tree<TItem> class as shown in bold in the code that follows. This method inserts a TItem value into the tree.

    The method definition should look like this:

    public class Tree<TItem> where TItem: IComparable<TItem>
    {
       ...
       public void Insert(TItem newItem)
       {
       }
    }

    The Insert method implements the recursive algorithm described earlier for creating an ordered binary tree. The programmer will have used the constructor to create the initial node of the tree (there is no default constructor), so the Insert method can assume that the tree is not empty. The code that follows is the part of the algorithm after checking whether the tree is empty. It’s reproduced here to help you understand the code you will write for the Insert method in the following steps:

    ...
    Examine the value of the node, N, of the tree, B
    If the value of N is greater than that of the new item, I
    Then
      If the left subtree of B is empty
      Then
        Construct a new left subtree of B with the item I as the node, and empty
        left and right subtrees
      Else
        Insert I into the left subtree of B
    End If
    ...
  10. In the Insert method, add a statement that declares a local variable of type TItem, called currentNodeValue. Initialize this variable to the value of the NodeData property of the tree, as shown in bold in the following example:

    public void Insert(TItem newItem)
    {
       TItem currentNodeValue = this.NodeData;
    }
  11. Add the if-else statement shown in bold in the following code to the Insert method after the definition of the currentNodeValue variable.

    This statement uses the CompareTo method of the IComparable<T> interface to determine whether the value of the current node is greater than that of the new item:

    public void Insert(TItem newItem)
    {
        TItem currentNodeValue = this.NodeData;
        if (currentNodeValue.CompareTo(newItem) > 0)
        {
           // Insert the new item into the left subtree
        }
        else
        {
           // Insert the new item into the right subtree
        }
    }
  12. In the if part of the code, immediately after the // Insert the new item into the left subtree comment, add the following statements:

    if (this.LeftTree == null)
    {
        this.LeftTree = new Tree<TItem>(newItem);
    }
    else
    {
        this.LeftTree.Insert(newItem);
    }

    These statements check whether the left subtree is empty. If so, a new tree is created using the new item and it is attached as the left subtree of the current node; otherwise, the new item is inserted into the existing left subtree by calling the Insert method recursively.

  13. In the else part of the outermost if-else statement, immediately after the // Insert the new item into the right subtree comment, add the equivalent code that inserts the new node into the right subtree:

    if (this.RightTree == null)
    {
        this.RightTree = new Tree<TItem>(newItem);
    }
    else
    {
        this.RightTree.Insert(newItem);
    }
  14. Add another public method called WalkTree to the Tree<TItem> class after the Insert method.

    This method walks through the tree, visiting each node in sequence, and generates a string representation of the data that the tree contains. The method definition should look like this:

    public string WalkTree()
    {
    }
  15. Add the statements shown in bold in the code that follows to the WalkTree method.

    These statements implement the algorithm described earlier for traversing a binary tree. As each node is visited, the node value is returned by the method to the string:

    public string WalkTree()
    {
        string result = "";
        if (this.LeftTree != null)
        {
            result = this.LeftTree.WalkTree();
        }
        result += String.Format(" {0} ", this.NodeData.ToString());
        if (this.RightTree != null)
        {
            result += this.RightTree.WalkTree();
        }
        return result;
    }
  16. On the Build menu, click Build Solution. The class should compile cleanly, so correct any errors that are reported and rebuild the solution if necessary.

In the next exercise, you will test the Tree<TItem> class by creating binary trees of integers and strings.

Test the Tree<TItem> class
  1. In Solution Explorer, right-click the BinaryTree solution, point to Add, and then click New Project.

    Note

    Ensure that you right-click the BinaryTree solution rather than the BinaryTree project.

  2. Add a new project by using the Console Application template. Name the project BinaryTreeTest. Set the Location to Microsoft PressVisual CSharp Step By StepChapter 17 in your Documents folder, and then click OK.

    Note

    A Visual Studio 2013 solution can contain more than one project. You are using this feature to add a second project to the BinaryTree solution for testing the Tree<TItem> class.

  3. In Solution Explorer, right-click the BinaryTreeTest project, and then click Set As Startup Project.

    The BinaryTreeTest project is highlighted in Solution Explorer. When you run the application, this is the project that will actually execute.

  4. In Solution Explorer, right-click the BinaryTreeTest project, and then click Add Reference.

  5. In the left pane of the Reference Manager - BinaryTreeTest dialog box, click Solution. In the middle pane, select the BinaryTree project (be sure to select the check box and not simply click the assembly), and then click OK.

    A screenshot of the Reference Manager dialog box.

    This step adds the BinaryTree assembly to the list of references for the BinaryTreeTest project in Solution Explorer. If you examine the References folder for the BinaryTreeTest project in Solution Explorer, you should see the BinaryTree assembly listed at the top. You will now be able to create Tree<TItem> objects in the BinaryTreeTest project.

    Note

    If the class library project is not part of the same solution as the project that uses it, you must add a reference to the assembly (the .dll file) and not to the class library project. You can do this by browsing for the assembly in the Reference Manager dialog box. You will use this technique in the final set of exercises in this chapter.

  6. In the Code and Text Editor window displaying the Program class in the program.cs file, add the following using directive to the list at the top of the class?

    using BinaryTree;
  7. Add the statements shown below in bold to Main method.

    static void Main(string[] args)
    {
        Tree<int> tree1 = new Tree<int>(10);
        tree1.Insert(5);
        tree1.Insert(11);
        tree1.Insert(5);
        tree1.Insert(-12);
        tree1.Insert(15);
        tree1.Insert(0);
        tree1.Insert(14);
        tree1.Insert(-8);
        tree1.Insert(10);
        tree1.Insert(8);
        tree1.Insert(8);
        string sortedData = tree1.WalkTree();
        Console.WriteLine("Sorted data is: {0}", sortedData);
    }

    These statements create a new binary tree for holding ints. The constructor creates an initial node containing the value 10. The Insert statements add nodes to the tree, and the WalkTree method generates a string representing the contents of the tree, which should appear sorted in ascending order when this string is displayed.

    Note

    Remember that the int keyword in C# is just an alias for the System.Int32 type; whenever you declare an int variable, you are actually declaring a struct variable of type System.Int32. The System.Int32 type implements the IComparable and IComparable<T> interfaces, which is why you can create Tree<int> objects. Similarly, the string keyword is an alias for System.String, which also implements IComparable and IComparable<T>.

  8. On the Build menu, click Build Solution, verify that the solution compiles, and then correct any errors, if necessary.

  9. On the Debug menu, click Start Without Debugging.

    Verify that the program runs and displays the values in the following sequence:

    –12 –8 0 5 5 8 8 10 10 11 14 15

  10. Press the Enter key to return to Visual Studio 2013.

  11. Add the following statements shown in bold to the end of the Main method in the Program class, after the existing code:

    static void Main(string[] args)
    {
        ...
        Tree<string> tree2 = new Tree<string>("Hello");
        tree2.Insert("World");
        tree2.Insert("How");
        tree2.Insert("Are");
        tree2.Insert("You");
        tree2.Insert("Today");
        tree2.Insert("I");
        tree2.Insert("Hope");
        tree2.Insert("You");
        tree2.Insert("Are");
        tree2.Insert("Feeling");
        tree2.Insert("Well");
        tree2.Insert("!");
        sortedData = tree2.WalkTree();
        Console.WriteLine("Sorted data is: {0}", sortedData);
    }

    These statements create another binary tree for holding strings, populate it with some test data, and then print the tree. This time, the data is sorted alphabetically.

  12. On the Build menu, click Build Solution, verify that the solution compiles, and then correct any errors, if necessary.

  13. On the Debug menu, click Start Without Debugging.

    Verify that the program runs and displays the integer values as before, followed by the strings in the following sequence:

    ! Are Are Feeling Hello Hope How I Today Well World You You

    A screenshot showing the sorted output generated by the BuildTree application.
  14. Press the Enter key to return to Visual Studio 2013.

Creating a generic method

As well as defining generic classes, you can create generic methods.

With a generic method, you can specify the types of the parameters and the return type by using a type parameter in a manner similar to that used when defining a generic class. In this way, you can define generalized methods that are type-safe and avoid the overhead of casting (and boxing, in some cases). Generic methods are frequently used in conjunction with generic classes; you need them for methods that take generic types as parameters or that have a return type that is a generic type.

You define generic methods by using the same type parameter syntax that you use when creating generic classes. (You can also specify constraints.) For example, the generic Swap<T> method in the code that follows swaps the values in its parameters. Because this functionality is useful regardless of the type of data being swapped, it is helpful to define it as a generic method:

static void Swap<T>(ref T first, ref T second)
{
   T temp = first;
   first = second;
   second = temp;
}

You invoke the method by specifying the appropriate type for its type parameter. The following examples show how to invoke the Swap<T> method to swap over two ints and two strings:

int a = 1, b = 2;
Swap<int>(ref a, ref b);
...
string s1 = "Hello", s2 = "World";
Swap<string>(ref s1, ref s2);

Note

Just as instantiating a generic class with different type parameters causes the compiler to generate different types, each distinct use of the Swap<T> method causes the compiler to generate a different version of the method. Swap<int> is not the same method as Swap<string>—both methods just happen to have been generated from the same generic template, so they exhibit the same behavior, albeit over different types.

Defining a generic method to build a binary tree

In the previous exercise, you created a generic class for implementing a binary tree. The Tree<TItem> class provides the Insert method for adding data items to the tree. However, if you want to add a large number of items, repeated calls to the Insert method are not very convenient. In the following exercise, you will define a generic method called InsertIntoTree that you can use to insert a list of data items into a tree with a single method call. You will test this method by using it to insert a list of characters into a tree of characters.

Write the InsertIntoTree method
  1. Using Visual Studio 2013, create a new project by using the Console Application template. In the New Project dialog box, name the project BuildTree. Set the Location to Microsoft PressVisual CSharp Step By StepChapter 17 in your Documents folder. In the Solution list, click Create New Solution, and then click OK.

  2. On the Project menu, click Add Reference. In the Reference Manager - BuildTree dialog box, click the Browse button (not the Browse tab in the left pane).

  3. In the Select The Files To Reference dialog box, move to the folder Microsoft PressVisual CSharp Step By StepChapter 17BinaryTreeBinaryTreeinDebug in your Documents folder, click BinaryTree.dll, and then click Add.

  4. In the Reference Manager – BuildTree dialog box, verify that the BinaryTree.dll assembly is listed and that the check box for this assembly is selected, and then click OK.

    The BinaryTree assembly is added to the list of references shown in Solution Explorer.

  5. In the Code and Text Editor window displaying the Program.cs file, add the following using directive to the top of the Program.cs file:

    using BinaryTree;

    Remember, this namespace contains the Tree<TItem> class.

  6. Add after the Main method a method called InsertIntoTree to the Program class. This should be a static void method that takes a Tree<TItem> parameter and a params array of TItem elements called data. The tree parameter should be passed by reference, for reasons that will be described in a later step.

    The method definition should look like this:

    static void InsertIntoTree<TItem>(ref Tree<TItem> tree,
        params TItem[] data)
    {
    }
  7. The TItem type used for the elements being inserted into the binary tree must implement the IComparable<TItem> interface. Modify the definition of the InsertIntoTree method and add the where clause shown in bold in the following code:

    static void InsertIntoTree<TItem>(ref Tree<TItem> tree,
        params TItem[] data) where TItem :IComparable<TItem>
    {
    }
  8. Add the statements shown in bold in the example that follows to the InsertIntoTree method:

    These statements iterate through the params list, adding each item to the tree by using the Insert method. If the value specified by the tree parameter is null initially, a new Tree<TItem> is created; this is why the tree parameter is passed by reference.

    static void InsertIntoTree<TItem>(ref Tree<TItem> tree,
    params TItem[] data) where TItem : IComparable<TItem>
    {
        foreach (TItem datum in data)
        {
            if (tree == null)
            {
                tree = new Tree<TItem>(datum);
            }
            else
            {
                tree.Insert(datum);
            }
        }
    }
Test the InsertIntoTree method
  1. In the Main method of the Program class, add the following statements shown in bold that create a new Tree for holding character data, populate it with some sample data by using the InsertIntoTree method, and then display it by using the WalkTree method of Tree:

    static void Main(string[] args)
    {
        Tree<char> charTree = null;
        InsertIntoTree<char>(ref charTree, 'M', 'X', 'A', 'M', 'Z', 'Z', 'N'),
        string sortedData = charTree.WalkTree();
        Console.WriteLine("Sorted data is: {0}", sortedData);
    }
  2. On the Build menu, click Build Solution, verify that the solution compiles, and then correct any errors if necessary.

  3. On the Debug menu, click Start Without Debugging.

    The program runs and displays the character values in the following order:

    A M M N X Z Z

  4. Press the Enter key to return to Visual Studio 2013.

Variance and generic interfaces

Chapter 8 demonstrates that you can use the object type to hold a value or reference of any other type. For example, the following code is completely legal:

string myString = "Hello";
object myObject = myString;

Remember that in inheritance terms, the String class is derived from the Object class, so all strings are objects.

Now, consider the following generic interface and class:

interface IWrapper<T>
{
    void SetData(T data);
    T GetData();
}
class Wrapper<T> : IWrapper<T>
{
    private T storedData;
    void IWrapper<T>.SetData(T data)
    {
        this.storedData = data;
    }
    T IWrapper<T>.GetData()
    {
        return this.storedData;
    }
}

The Wrapper<T> class provides a simple wrapper around a specified type. The IWrapper interface defines the SetData method that the Wrapper<T> class implements to store the data and the GetData method that the Wrapper<T> class implements to retrieve the data. You can create an instance of this class and use it to wrap a string like this:

Wrapper<string> stringWrapper = new Wrapper<string>();
IWrapper<string> storedStringWrapper = stringWrapper;
storedStringWrapper.SetData("Hello");
Console.WriteLine("Stored value is {0}", storedStringWrapper.GetData());

The code creates an instance of the Wrapper<string> type. It references the object through the IWrapper<string> interface to call the SetData method. (The Wrapper<T> type implements its interfaces explicitly, so you must call the methods through an appropriate interface reference.) The code also calls the GetData method through the IWrapper<string> interface. If you run this code, it outputs the message “Stored value is Hello”.

Take a look at the following line of code:

IWrapper<object> storedObjectWrapper = stringWrapper;

This statement is similar to the one that creates the IWrapper<string> reference in the previous code example, the difference being that the type parameter is object rather than string. Is this code legal? Remember that all strings are objects (you can assign a string value to an object reference, as shown earlier), so in theory this statement looks promising. However, if you try it, the statement will fail to compile with the message “Cannot implicitly convert type ‘Wrapper<string>’ to ‘IWrapper<object>’.”

You can try an explicit cast such as this:

IWrapper<object> storedObjectWrapper = (IWrapper<object>)stringWrapper;

This code compiles but will fail at run time with an InvalidCastException exception. The problem is that although all strings are objects, the converse is not true. If this statement were allowed, you could write code like this, which ultimately attempts to store a Circle object in a string field:

IWrapper<object> storedObjectWrapper = (IWrapper<object>)stringWrapper;
Circle myCircle = new Circle();
storedObjectWrapper.SetData(myCircle);

The IWrapper<T> interface is said to be invariant. You cannot assign an IWrapper<A> object to a reference of type IWrapper<B>, even if type A is derived from type B. By default, C# implements this restriction to ensure the type safety of your code.

Covariant interfaces

Suppose that you defined the IStoreWrapper<T> and IRetrieveWrapper<T> interfaces, shown in the following example, in place of IWrapper<T> and implemented these interfaces in the Wrapper<T> class, like this:

interface IStoreWrapper<T>
{
    void SetData(T data);
}
interface IRetrieveWrapper<T>
{
    T GetData();
}
class Wrapper<T> : IStoreWrapper<T>, IRetrieveWrapper<T>
{
   private T storedData;
   void IStoreWrapper<T>.SetData(T data)
   {
       this.storedData = data;
   }
   T IRetrieveWrapper<T>.GetData()
   {
       return this.storedData;
   }
}

Functionally, the Wrapper<T> class is the same as before, except that you access the SetData and GetData methods through different interfaces.

Wrapper<string> stringWrapper = new Wrapper<string>();
IStoreWrapper<string> storedStringWrapper = stringWrapper;
storedStringWrapper.SetData("Hello");
IRetrieveWrapper<string> retrievedStringWrapper = stringWrapper;
Console.WriteLine("Stored value is {0}", retrievedStringWrapper.GetData());

Thus, is the following code legal?

IRetrieveWrapper<object> retrievedObjectWrapper = stringWrapper;

The quick answer is no, and it fails to compile with the same error as before. But, if you think about it, although the C# compiler has deemed that this statement is not type safe, the reasons for assuming this are no longer valid. The IRetrieveWrapper<T> interface only allows you to read the data held in the Wrapper<T> object by using the GetData method, and it does not provide any way to change the data. In situations such as this where the type parameter occurs only as the return value of the methods in a generic interface, you can inform the compiler that some implicit conversions are legal and that it does not have to enforce strict type-safety. You do this by specifying the out keyword when you declare the type parameter, like this:

interface IRetrieveWrapper<out T>
{
    T GetData();
}

This feature is called covariance. You can assign an IRetrieveWrapper<A> object to an IRetrieveWrapper<B> reference as long as there is a valid conversion from type A to type B, or type A derives from type B. The following code now compiles and runs as expected:

// string derives from object, so this is now legal
IRetrieveWrapper<object> retrievedObjectWrapper = stringWrapper;

You can specify the out qualifier with a type parameter only if the type parameter occurs as the return type of methods. If you use the type parameter to specify the type of any method parameters, the out qualifier is illegal and your code will not compile. Also, covariance works only with reference types. This is because value types cannot form inheritance hierarchies. So, the following code will not compile because int is a value type:

Wrapper<int> intWrapper = new Wrapper<int>();
IStoreWrapper<int> storedIntWrapper = intWrapper; // this is legal
...
// the following statement is not legal - ints are not objects
IRetrieveWrapper<object> retrievedObjectWrapper = intWrapper;

Several of the interfaces defined by the .NET Framework exhibit covariance, including the IEnumerable<T> interface, which is detailed in Chapter 19.

Note

Only interface and delegate types (which are covered in Chapter 18) can be declared as covariant. You do not specify the out modifier with generic classes.

Contravariant interfaces

Contravariance follows a similar principle to covariance except that it works in the opposite direction; it enables you to use a generic interface to reference an object of type B through a reference to type A as long as type B derives type A. This sounds complicated, so it is worth looking at an example from the .NET Framework class library.

The System.Collections.Generic namespace in the .NET Framework provides an interface called IComparer, which looks like this:

public interface IComparer<in T>
{
    int Compare(T x, T y);
}

A class that implements this interface has to define the Compare method, which is used to compare two objects of the type specified by the T type parameter. The Compare method is expected to return an integer value: zero if the parameters x and y have the same value, negative if x is less than y, and positive if x is greater than y. The following code shows an example that sorts objects according to their hash code. (The GetHashCode method is implemented by the Object class. It simply returns an integer value that identifies the object. All reference types inherit this method and can override it with their own implementations.)

class ObjectComparer : IComparer<Object>
{
    int IComparer<Object>.Compare(Object x, Object y)
    {
        int xHash = x.GetHashCode();
        int yHash = y.GetHashCode();
        if (xHash == yHash)
            return 0;
        if (xHash < yHash)
            return -1;
        return 1;
    }
}

You can create an ObjectComparer object and call the Compare method through the IComparer<Object> interface to compare two objects, like this:

Object x = ...;
Object y = ...;
ObjectComparer objectComparer = new ObjectComparer();
IComparer<Object> objectComparator = objectComparer;
int result = objectComparator.Compare(x, y);

That’s the boring bit. What is more interesting is that you can reference this same object through a version of the IComparer interface that compares strings, like this:

IComparer<String> stringComparator = objectComparer;

At first glance, this statement seems to break every rule of type safety that you can imagine. However, if you think about what the IComparer<T> interface does, this approach makes sense. The purpose of the Compare method is to return a value based on a comparison between the parameters passed in. If you can compare Objects, you certainly should be able to compare Strings, which are just specialized types of Objects. After all, a String should be able to do anything that an Object can do—that is the purpose of inheritance.

This still sounds a little presumptive, however. How does the C# compiler know that you are not going to perform any type-specific operations in the code for the Compare method that might fail if you invoke the method through an interface based on a different type? If you revisit the definition of the IComparer interface, you can see the in qualifier prior to the type parameter:

public interface IComparer<in T>
{
    int Compare(T x, T y);
}

The in keyword tells the C# compiler that either you can pass the type T as the parameter type to methods or you can pass any type that derives from T. You cannot use T as the return type from any methods. Essentially, this makes it possible for you to reference an object either through a generic interface based on the object type or through a generic interface based on a type that derives from the object type. Basically, if a type A exposes some operations, properties, or fields, in that case if type B derives from type A, it must also expose the same operations (which might behave differently if they have been overridden), properties, and fields. Consequently, it should be safe to substitute an object of type B for an object of type A.

Covariance and contravariance might seem like fringe topics in the world of generics, but they are useful. For example, the List<T> generic collection class (in the System.Collections.Generic namespace) uses IComparer<T> objects to implement the Sort and BinarySearch methods. A List<Object> object can contain a collection of objects of any type, so the Sort and BinarySearch methods need to be able to sort objects of any type. Without using contravariance, the Sort and BinarySearch methods would need to include logic that determines the real types of the items being sorted or searched and then implement a type-specific sort or search mechanism. However, unless you are a mathematician, it can be quite difficult to recall what covariance and contravariance actually do. The way I remember, based on the examples in this section, is as follows:

  • Covariance example. If the methods in a generic interface can return strings, they can also return objects. (All strings are objects.)

  • Contravariance example. If the methods in a generic interface can take object parameters, they can take string parameters. (If you can perform an operation by using an object, you can perform the same operation by using a string because all strings are objects.)

Note

As with covariance, only interface and delegate types can be declared as contravariant. You do not specify the in modifier with generic classes.

Summary

In this chapter, you learned how to use generics to create type-safe classes. You saw how to instantiate a generic type by specifying a type parameter. You also saw how to implement a generic interface and define a generic method. Finally, you learned how to define covariant and contravariant generic interfaces that can operate with a hierarchy of types.

  • If you want to continue to the next chapter, keep Visual Studio 2013 running, and turn to Chapter 18.

  • If you want to exit Visual Studio 2013 now, on the File menu, click Exit. If you see a Save dialog box, click Yes and save the project.

Quick reference

To

Do this

Instantiate an object by using a generic type

Specify the appropriate generic type parameter. For example:

Queue<int> myQueue = new Queue<int>();

Create a new generic type

Define the class by using a type parameter. For example:

public class Tree<TItem>
{
    ...
}

Restrict the type that can be substituted for the generic type parameter

Specify a constraint by using a where clause when defining the class. For example:

public class Tree<TItem>
where TItem : IComparable<TItem>
{
    ...
}

Define a generic method

Define the method by using type parameters. For example:

static void InsertIntoTree<TItem>
(Tree<TItem> tree, params TItem[] data)
{
    ...
}

Invoke a generic method

Provide types for each of the type parameters. For example:

InsertIntoTree<char>(charTree, 'Z', 'X'),

Define a covariant interface

Specify the out qualifier for covariant type parameters. Reference the covariant type parameters only as the return types from methods and not as the types for method parameters:

interface IRetrieveWrapper<out T>
{
    T GetData();
}

Define a contravariant interface

Specify the in qualifier for contravariant type parameters. Reference the contravariant type parameters only as the types of method parameters and not as return types:

public interface IComparer<in T>
{
   int Compare(T x, T y);
}
..................Content has been hidden....................

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