Chapter 22. Generics

 

...our special individuality, as distinguished from our generic humanity.

 
 --Oliver Wendell Holmes, Sr.
 

Every man of genius sees the world at a different angle from his fellows.

 
 --Havelock Ellis
 

Born under one law, to another bound.

 
 --Lord Brooke
<feature> <supertitle>Objectives</supertitle>

In this chapter you’ll learn:

<objective>

To create generic methods that perform identical tasks on arguments of different types.

</objective>
<objective>

To create a generic Stack class that can be used to store objects of any class or interface type.

</objective>
<objective>

To understand how to overload generic methods with nongeneric methods or with other generic methods.

</objective>
<objective>

To understand the new() constraint of a type parameter.

</objective>
<objective>

To apply multiple constraints to a type parameter.

</objective>
</feature>
<feature> <supertitle>Outline</supertitle> </feature>

Introduction

In Chapter 21, we presented data structures that stored and manipulated object references. This chapter continues our multi-chapter discussion on data structures. You could store any object in our data structures. One inconvenient aspect of storing object references occurs when retrieving them from a collection. An application normally needs to process specific types of objects. As a result, the object references obtained from a collection typically need to be downcast to an appropriate type to allow the application to process the objects correctly. In addition, data of value types (e.g., int and double) must be boxed to be manipulated with object references, which increases the overhead of processing such data. Most importantly, processing all data as type object limits the C# compiler’s ability to perform type checking.

Though we can easily create data structures that manipulate any type of data as objects (as we did in Chapter 21), it would be nice if we could detect type mismatches at compile time—this is known as compile-time type safety. For example, if a Stack should store only int values, attempting to push a string onto that Stack should cause a compile-time error. Similarly, a Sort method should be able to compare elements that are all guaranteed to have the same type. If we create type-specific versions of class Stack class and method Sort, the C# compiler would certainly be able to ensure compile-time type safety. However, this would require that we create many copies of the same basic code.

This chapter discusses generics, which provide the means to create the general models mentioned above. Generic methods enable you to specify, with a single method declaration, a set of related methods. Generic classes enable you to specify, with a single class declaration, a set of related classes. Similarly, generic interfaces enable you to specify, with a single interface declaration, a set of related interfaces. Generics provide compile-time type safety. [Note: You can also implement generic structs and delegates.] So far in this book, we’ve used the generic types List (Chapter 9) and Dictionary (Chapter 17).

We can write a generic method for sorting an array of objects, then invoke the generic method separately with an int array, a double array, a string array and so on, to sort each different type of array. The compiler performs type checking to ensure that the array passed to the sorting method contains only elements of the correct type. We can write a single generic Stack class that manipulates a stack of objects, then instantiate Stack objects for a stack of ints, a stack of doubles, a stack of strings and so on. The compiler performs type checking to ensure that the Stack stores only elements of the correct type.

This chapter presents examples of generic methods and generic classes. It also considers the relationships between generics and other C# features, such as overloading. Chapter 23, Collections, discusses the .NET Framework’s generic and nongeneric collections classes. A collection is a data structure that maintains a group of related objects or values. The .NET Framework collection classes use generics to allow you to specify the exact types of object that a particular collection will store.

Motivation for Generic Methods

Overloaded methods are often used to perform similar operations on different types of data. To understand the motivation for generic methods, let’s begin with an example (Fig. 22.1) that contains three overloaded DisplayArray methods (lines 23–29, lines 32–38 and lines 41–47). These methods display the elements of an int array, a double array and a char array, respectively. Soon, we’ll reimplement this program more concisely and elegantly using a single generic method.

Example 22.1. Using overloaded methods to display arrays of different types.

 1   // Fig. 22.1: OverloadedMethods.cs
 2   // Using overloaded methods to display arrays of different types.
 3   using System;
 4
 5   class OverloadedMethods
 6   {
 7      static void Main( string[] args )
 8      {
 9         // create arrays of int, double and char
10         int[] intArray = { 1, 2, 3, 4, 5, 6 };
11         double[] doubleArray = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7 };
12         char[] charArray = { 'H', 'E', 'L', 'L', 'O' };
13
14         Console.WriteLine( "Array intArray contains:" );
15         DisplayArray( intArray ); // pass an int array argument
16         Console.WriteLine( "Array doubleArray contains:" );
17         DisplayArray( doubleArray ); // pass a double array argument
18         Console.WriteLine( "Array charArray contains:" );
19         DisplayArray( charArray ); // pass a char array argument
20      } // end Main
21
22      // output int array                                 
23      private static void DisplayArray( int[] inputArray )
24      {                                                   
25         foreach ( int element in inputArray )            
26            Console.Write( element + " " );               
27                                                          
28         Console.WriteLine( "
" );                       
29      } // end method DisplayArray                        
30
31      // output double array                                 
32      private static void DisplayArray( double[] inputArray )
33      {                                                      
34         foreach ( double element in inputArray )            
35            Console.Write( element + " " );                  
36                                                             
37         Console.WriteLine( "
" );                          
38      } // end method DisplayArray                           
39
40      // output char array                                 
41      private static void DisplayArray( char[] inputArray )
42      {                                                    
43         foreach ( char element in inputArray )            
44            Console.Write( element + " " );                
45                                                           
46         Console.WriteLine( "
" );                        
47      } // end method DisplayArray                         
48   } // end class OverloadedMethods
Array intArray contains:
1 2 3 4 5 6

Array doubleArray contains:
1.1 2.2 3.3 4.4 5.5 6.6 7.7

Array charArray contains:
H E L L O

The program begins by declaring and initializing three arrays—six-element int array intArray (line 10), seven-element double array doubleArray (line 11) and five-element char array charArray (line 12). Then, lines 14–19 output the arrays.

When the compiler encounters a method call, it attempts to locate a method declaration that has the same method name and parameters that match the argument types in the method call. In this example, each DisplayArray call exactly matches one of the DisplayArray method declarations. For example, line 15 calls DisplayArray with intArray as its argument. At compile time, the compiler determines argument intArray’s type (i.e., int[]), attempts to locate a method named DisplayArray that specifies a single int[] parameter (which it finds at lines 23–29) and sets up a call to that method. Similarly, when the compiler encounters the DisplayArray call at line 17, it determines argument doubleArray’s type (i.e., double[]), then attempts to locate a method named DisplayArray that specifies a single double[] parameter (which it finds at lines 32–38) and sets up a call to that method. Finally, when the compiler encounters the DisplayArray call at line 19, it determines argument charArray’s type (i.e., char[]), then attempts to locate a method named DisplayArray that specifies a single char[] parameter (which it finds at lines 41–47) and sets up a call to that method.

Study each DisplayArray method. Note that the array element type (int, double or char) appears in two locations in each method—the method header (lines 23, 32 and 41) and the foreach statement header (lines 25, 34 and 43). If we replace the element types in each method with a generic name (such as T for “type”) then all three methods would look like the one in Fig. 22.2. It appears that if we can replace the array element type in each of the three methods with a single “generic type parameter,” then we should be able to declare one DisplayArray method that can display the elements of any array. The method in Fig. 22.2 will not compile, because its syntax is not correct. We declare a generic DisplayArray method with the proper syntax in Fig. 22.3.

Example 22.2. DisplayArray method in which actual type names are replaced by convention with the generic name T.

 1   private static void DisplayArray( T[] inputArray )
 2   {
 3      foreach ( T element in inputArray )
 4         Console.Write( element + " " );
 5
 6      Console.WriteLine( "
" );
 7   } // end method DisplayArray

Example 22.3. Using a generic method to display arrays of different types.

 1   // Fig. 22.3: GenericMethod.cs
 2   // Using overloaded methods to display arrays of different types.
 3   using System;
 4   using System.Collections.Generic;
 5
 6   class GenericMethod
 7   {
 8      public static void Main( string[] args )
 9      {
10         // create arrays of int, double and char
11         int[] intArray = { 1, 2, 3, 4, 5, 6 };
12         double[] doubleArray = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7 };
13         char[] charArray = { 'H', 'E', 'L', 'L', 'O' };
14
15         Console.WriteLine( "Array intArray contains:" );
16         DisplayArray( intArray ); // pass an int array argument
17         Console.WriteLine( "Array doubleArray contains:" );
18         DisplayArray( doubleArray ); // pass a double array argument
19         Console.WriteLine( "Array charArray contains:" );
20         DisplayArray( charArray ); // pass a char array argument
21      } // end Main
22
23      // output array of all types                           
24      private static void DisplayArray< T >( T[] inputArray )
25      {                                                      
26         foreach ( T element in inputArray )                 
27            Console.Write( element + " " );                  
28                                                             
29         Console.WriteLine( "
" );                          
30      } // end method DisplayArray                           
31   } // end class GenericMethod

Array intArray contains:
1 2 3 4 5 6

Array doubleArray contains:
1.1 2.2 3.3 4.4 5.5 6.6 7.7

Array charArray contains:
H E L L O

Generic-Method Implementation

If the operations performed by several overloaded methods are identical for each argument type, the overloaded methods can be more compactly and conveniently coded using a generic method. You can write a single generic-method declaration that can be called at different times with arguments of different types. Based on the types of the arguments passed to the generic method, the compiler handles each method call appropriately.

Figure 22.3 reimplements the application of Fig. 22.1 using a generic DisplayArray method (lines 24–30). Note that the DisplayArray method calls in lines 16, 18 and 20 are identical to those of Fig. 22.1, the outputs of the two applications are identical and the code in Fig. 22.3 is 17 lines shorter than that in Fig. 22.1. As illustrated in Fig. 22.3, generics enable us to create and test our code once, then reuse it for many different types of data. This demonstrates the expressive power of generics.

Line 24 begins method DisplayArray’s declaration. All generic method declarations have a type-parameter list delimited by angle brackets (<T> in this example) that follows the method’s name. Each type-parameter list contains one or more type parameters, separated by commas. A type parameter is an identifier that’s used in place of actual type names. The type parameters can be used to declare the return type, the parameter types and the local variable types in a generic method declaration; the type parameters act as placeholders for type arguments that represent the types of data that will be passed to the generic method. A generic method’s body is declared like that of any other method. Note that the type-parameter names throughout the method declaration must match those declared in the type-parameter list. For example, line 26 declares element in the foreach statement as type T, which matches the type parameter (T) declared in line 24. Also, a type parameter can be declared only once in the type-parameter list but can appear more than once in the method’s parameter list. Type-parameter names need not be unique among different generic methods.

Common Programming Error 22.1

Common Programming Error 22.1

If you forget to include the type-parameter list when declaring a generic method, the compiler will not recognize the type-parameter names when they’re encountered in the method. This results in compilation errors.

Method DisplayArray’s type-parameter list (line 24) declares type parameter T as the placeholder for the array-element type that DisplayArray will output. Note that T appears in the parameter list as the array-element type (line 24). The foreach statement header (line 26) also uses T as the element type. These are the same two locations where the overloaded DisplayArray methods of Fig. 22.1 specified int, double or char as the element type. The remainder of DisplayArray is identical to the version presented in Fig. 22.1.

Good Programming Practice 22.1

Good Programming Practice 22.1

It’s recommended that type parameters be specified as individual capital letters. Typically, a type parameter that represents the type of an element in an array (or other collection) is named E for “element” or T for “type.”

As in Fig. 22.1, the program of Fig. 22.3 begins by declaring and initializing six-element int array intArray (line 11), seven-element double array doubleArray (line 12) and five-element char array charArray (line 13). Then each array is output by calling DisplayArray (lines 16, 18 and 20)—once with argument intArray, once with argument doubleArray and once with argument charArray.

When the compiler encounters a method call such as line 16, it analyzes the set of methods (both nongeneric and generic) that might match the method call, looking for a method that best matches the call. If there are no matching methods, or if there’s more than one best match, the compiler generates an error. If you have any uncertainty on which of your methods will be called, the complete details of method-call resolution can be found in Section 14.5.5.1 of the Ecma C# Language Specification

www.ecma-international.org/publications/standards/Ecma-334.htm

or Section 7.5.3 of the Microsoft C# Language Specification 4

bit.ly/CSharp4Spec

In the case of line 16, the compiler determines that the best match occurs if the type parameter T in lines 24 and 26 of method DisplayArray’s declaration is replaced with the type of the elements in the method call’s argument intArray (i.e., int). Then, the compiler sets up a call to DisplayArray with the int as the type argument for the type parameter T. This is known as the type-inferencing process. The same process is repeated for the calls to method DisplayArray in lines 18 and 20.

Common Programming Error 22.2

Common Programming Error 22.2

If the compiler cannot find a single nongeneric or generic method declaration that’s a best match for a method call, or if there are multiple best matches, a compilation error occurs.

You can also use explicit type arguments to indicate the exact type that should be used to call a generic function. For example, line 16 could be written as

DisplayArray< int >( intArray ); // pass an int array argument

The preceding method call explicitly provides the type argument (int) that should be used to replace type parameter T in lines 24 and 26 of the DisplayArray method’s declaration.

For each variable declared with a type parameter, the compiler also determines whether the operations performed on such a variable are allowed for all types that the type parameter can assume. The only operation performed on the array elements in this example is to output the string representation of the elements. Line 27 performs an implicit boxing conversion for every value-type array element and an implicit ToString call on every array element. Since all objects have a ToString method, the compiler is satisfied that line 27 performs a valid operation for any array element.

By declaring DisplayArray as a generic method in Fig. 22.3, we eliminated the need for the overloaded methods of Fig. 22.1, saving 17 lines of code and creating a reusable method that can output the string representations of the elements in any one-dimensional array, not just arrays of int, double or char elements.

Type Constraints

In this section, we present a generic Maximum method that determines and returns the largest of its three arguments (all of the same type). The generic method in this example uses the type parameter to declare both the method’s return type and its parameters. Normally, when comparing values to determine which one is greater, you would use the > operator. However, this operator is not overloaded for use with every type that’s built into the Framework Class Library or that might be defined by extending those types. Generic code is restricted to performing operations that are guaranteed to work for every possible type. Thus, an expression like variable1 < variable2 is not allowed unless the compiler can ensure that the operator < is provided for every type that will ever be used in the generic code. Similarly, you cannot call a method on a generic-type variable unless the compiler can ensure that all types that will ever be used in the generic code support that method.

IComparable<T> Interface

It’s possible to compare two objects of the same type if that type implements the generic interface IComparable<T> (of namespace System). A benefit of implementing interface IComparable<T> is that IComparable<T> objects can be used with the sorting and searching methods of classes in the System.Collections.Generic namespace—we discuss those methods in Chapter 23. The structures in the Framework Class Library that correspond to the simple types all implement this interface. For example, the structure for simple type double is Double and the structure for simple type int is Int32—both Double and Int32 implement the IComparable<T> interface. Types that implement IComparable<T> must declare a CompareTo method for comparing objects. For example, if we have two ints, int1 and int2, they can be compared with the expression:

int1.CompareTo( int2 )

Method CompareTo must return 0 if the objects are equal, a negative integer if int1 is less than int2 or a positive integer if int1 is greater than int2. It’s the responsibility of the programmer who declares a type that implements IComparable<T> to define method CompareTo such that it compares the contents of two objects of that type and returns the appropriate result.

Specifying Type Constraints

Even though IComparable objects can be compared, they cannot be used with generic code by default, because not all types implement interface IComparable<T>. However, we can restrict the types that can be used with a generic method or class to ensure that they meet certain requirements. This feature—known as a type constraint—restricts the type of the argument supplied to a particular type parameter. Figure 22.4 declares method Maximum (lines 20–34) with a type constraint that requires each of the method’s arguments to be of type IComparable<T>. This restriction is important, because not all objects can be compared. However, all IComparable<T> objects are guaranteed to have a CompareTo method that can be used in method Maximum to determine the largest of its three arguments.

Example 22.4. Generic method Maximum returns the largest of three objects.

 1   // Fig. 22.4: MaximumTest.cs
 2   // Generic method Maximum returns the largest of three objects.
 3   using System;
 4
 5   class MaximumTest
 6   {
 7      public static void Main( string[] args )
 8      {
 9         Console.WriteLine( "Maximum of {0}, {1} and {2} is {3}
",
10            3, 4, 5, Maximum( 3, 4, 5 ) );
11         Console.WriteLine( "Maximum of {0}, {1} and {2} is {3}
",
12            6.6, 8.8, 7.7, Maximum( 6.6, 8.8, 7.7 ) );
13         Console.WriteLine( "Maximum of {0}, {1} and {2} is {3}
",
14            "pear", "apple", "orange",
15            Maximum( "pear", "apple", "orange" ) );
16      } // end Main
17
18      // generic function determines the
19      // largest of the IComparable objects
20      private static T Maximum< T >( T x, T y, T z )
21         where T : IComparable< T >
22      {
23         T max = x; // assume x is initially the largest
24
25         // compare y with max
26         if ( y.CompareTo( max ) > 0 )
27            max = y; // y is the largest so far
28
29         // compare z with max
30         if ( z.CompareTo( max ) > 0 )
31            max = z; // z is the largest
32
33         return max; // return largest object
34      } // end method Maximum
35   } // end class MaximumTest

Maximum of 3, 4 and 5 is 5

Maximum of 6.6, 8.8 and 7.7 is 8.8

Maximum of pear, apple and orange is pear

Generic method Maximum uses type parameter T as the return type of the method (line 20), as the type of method parameters x, y and z (line 20), and as the type of local variable max (line 23). Generic method Maximum’s where clause (after the parameter list in line 21) specifies the type constraint for type parameter T. In this case, the clause where T : IComparable<T> indicates that this method requires the type argument to implement interface IComparable<T>. If no type constraint is specified, the default type constraint is object.

C# provides several kinds of type constraints. A class constraint indicates that the type argument must be an object of a specific base class or one of its subclasses. An interface constraint indicates that the type argument’s class must implement a specific interface. The type constraint in line 20 is an interface constraint, because IComparable<T> is an interface. You can specify that the type argument must be a reference type or a value type by using the reference-type constraint (class) or the value-type constraint (struct), respectively. Finally, you can specify a constructor constraintnew()—to indicate that the generic code can use operator new to create new objects of the type represented by the type parameter. If a type parameter is specified with a constructor constraint, the type argument’s class must provide a public parameterless or default constructor to ensure that objects of the class can be created without passing constructor arguments; otherwise, a compilation error occurs.

It’s possible to apply multiple constraints to a type parameter. To do so, simply provide a comma-separated list of constraints in the where clause. If you have a class constraint, reference-type constraint or value-type constraint, it must be listed first—only one of these types of constraints can be used for each type parameter. Interface constraints (if any) are listed next. The constructor constraint is listed last (if there is one).

Analyzing the Code

Method Maximum assumes that its first argument (x) is the largest and assigns it to local variable max (line 23). Next, the if statement at lines 26–27 determines whether y is greater than max. The condition invokes y’s CompareTo method with the expression y.CompareTo(max). If y is greater than max, then y is assigned to variable max (line 27). Similarly, the statement at lines 30–31 determines whether z is greater than max. If so, line 31 assigns z to max. Then, line 33 returns max to the caller.

In Main (lines 7–16), line 10 calls Maximum with the integers 3, 4 and 5. Generic method Maximum is a match for this call, but its arguments must implement interface IComparable<T> to ensure that they can be compared. Type int is a synonym for struct Int32, which implements interface IComparable<int>. Thus, ints (and other simple types) are valid arguments to method Maximum.

Line 12 passes three double arguments to Maximum. Again, this is allowed because double is a synonym for the Double struct, which implements IComparable<double>. Line 15 passes Maximum three strings, which are also IComparable<string> objects. Note that we intentionally placed the largest value in a different position in each method call (lines 10, 12 and 15) to show that the generic method always finds the maximum value, regardless of its position in the argument list and regardless of the inferred type argument.

Overloading Generic Methods

A generic method may be overloaded. Each overloaded method must have a unique signature (as discussed in Chapter 7). A class can provide two or more generic methods with the same name but different method parameters. For example, we could provide a second version of generic method DisplayArray (Fig. 22.3) with the additional parameters lowIndex and highIndex that specify the portion of the array to output (see Exercise 22.8).

A generic method can be overloaded by nongeneric methods with the same method name. When the compiler encounters a method call, it searches for the method declaration that best matches the method name and the argument types specified in the call. For example, generic method DisplayArray of Fig. 22.3 could be overloaded with a version specific to strings that outputs the strings in neat, tabular format (see Exercise 22.9). If the compiler cannot match a method call to either a nongeneric method or a generic method, or if there’s ambiguity due to multiple possible matches, the compiler generates an error.

Generic Classes

The concept of a data structure (e.g., a stack) that contains data elements can be understood independently of the element type it manipulates. A generic class provides a means for describing a class in a type-independent manner. We can then instantiate type-specific versions of the generic class. This capability is an opportunity for software reusability.

With a generic class, you can use a simple, concise notation to indicate the actual type(s) that should be used in place of the class’s type parameter(s). At compilation time, the compiler ensures your code’s type safety, and the runtime system replaces type parameters with type arguments to enable your client code to interact with the generic class.

One generic Stack class, for example, could be the basis for creating many Stack classes (e.g., “Stack of double,” “Stack of int,” “Stack of char,” “Stack of Employee”). Figure 22.5 presents a generic Stack class declaration. This class should not be confused with the class Stack from namespace System.Collections.Generics. A generic class declaration is similar to a nongeneric class declaration, except that the class name is followed by a type-parameter list (line 5) and, optionally, one or more constraints on its type parameter. Type parameter T represents the element type the Stack will manipulate. As with generic methods, the type-parameter list of a generic class can have one or more type parameters separated by commas. (You’ll create a generic class with two type parameters in Exercise 22.11.) Type parameter T is used throughout the Stack class declaration (Fig. 22.5) to represent the element type. Class Stack declares variable elements as an array of type T (line 8). This array (created at line 21) will store the Stack’s elements. [Note: This example implements a Stack as an array. As you’ve seen in Chapter 21, Stacks also are commonly implemented as linked lists.]

Example 22.5. Generic class Stack.

 1   // Fig. 22.5: Stack.cs
 2   // Generic class Stack.
 3   using System;
 4
 5   class Stack< T >
 6   {
 7      private int top; // location of the top element
 8      private T[] elements; // array that stores stack elements
 9
10      // parameterless constructor creates a stack of the default size
11      public Stack()
12         : this( 10 ) // default stack size
13      {
14         // empty constructor; calls constructor at line 18 to perform init
15      } // end stack constructor
16
17      // constructor creates a stack of the specified number of elements
18      public Stack( int stackSize )
19      {
20         if ( stackSize > 0 ) // validate stackSize
21            elements = new T[ stackSize ]; // create stackSize elements
22         else
23            throw new ArgumentException( "Stack size must be positive." );
24
25         top = -1; // stack initially empty
26      } // end stack constructor
27
28      // push element onto the stack; if unsuccessful,
29      // throw FullStackException
30      public void Push( T pushValue )
31      {
32         if ( top == elements.Length - 1 ) // stack is full
33            throw new FullStackException( string.Format(
34               "Stack is full, cannot push {0}", pushValue ) );
35
36         ++top; // increment top
37         elements[ top ] = pushValue; // place pushValue on stack
38      } // end method Push
39
40      // return the top element if not empty,
41      // else throw EmptyStackException
42      public T Pop()
43      {
44         if ( top == -1 ) // stack is empty
45            throw new EmptyStackException( "Stack is empty, cannot pop" );
46
47         --top; // decrement top
48         return elements[ top + 1 ]; // return top value
49      } // end method Pop
50   } // end class Stack

Class Stack has two constructors. The parameterless constructor (lines 11–15) passes the default stack size (10) to the one-argument constructor, using the syntax this (line 12) to invoke another constructor in the same class. The one-argument constructor (lines 18–26) validates the stackSize argument and creates an array of the specified stackSize (if it’s greater than 0) or throws an exception, otherwise.

Method Push (lines 30–38) first determines whether an attempt is being made to push an element onto a full Stack. If so, lines 33–34 throw a FullStackException (declared in Fig. 22.6). If the Stack is not full, line 36 increments the top counter to indicate the new top position, and line 37 places the argument in that location of array elements.

Example 22.6. FullStackException indicates a stack is full.

 1   // Fig. 22.6: FullStackException.cs
 2   // FullStackException indicates a stack is full.
 3   using System;
 4
 5   class FullStackException : Exception
 6   {
 7      // parameterless constructor
 8      public FullStackException() : base( "Stack is full" )
 9      {
10         // empty constructor
11      } // end FullStackException constructor
12
13      // one-parameter constructor
14      public FullStackException( string exception ) : base( exception )
15      {
16         // empty constructor
17      } // end FullStackException constructor
18
19      // two-parameter constructor
20      public FullStackException( string exception, Exception inner )
21         : base( exception, inner )
22      {
23         // empty constructor
24      } // end FullStackException constructor
25   } // end class FullStackException

Method Pop (lines 42–49) first determines whether an attempt is being made to pop an element from an empty Stack. If so, line 45 throws an EmptyStackException (declared in Fig. 22.7). Otherwise, line 47 decrements the top counter to indicate the new top position, and line 48 returns the original top element of the Stack.

Example 22.7. EmptyStackException indicates a stack is empty.

 1   // Fig. 22.7: EmptyStackException.cs
 2   // EmptyStackException indicates a stack is empty.
 3   using System;
 4
 5   class EmptyStackException : Exception
 6   {
 7      // parameterless constructor
 8      public EmptyStackException() : base( "Stack is empty" )
 9      {
10         // empty constructor
11      } // end EmptyStackException constructor
12
13      // one-parameter constructor
14      public EmptyStackException( string exception ) : base( exception )
15      {
16         // empty constructor
17      } // end EmptyStackException constructor
18
19      // two-parameter constructor
20      public EmptyStackException( string exception, Exception inner )
21         : base( exception, inner )
22      {
23         // empty constructor
24      } // end EmptyStackException constructor
25   } // end class EmptyStackException

Classes FullStackException (Fig. 22.6) and EmptyStackException (Fig. 22.7) each provide a parameterless constructor, a one-argument constructor of exception classes (as discussed in Section 13.8) and a two-argument constructor for creating a new exception using an existing one. The parameterless constructor sets the default error message while the other two constructors set custom error messages.

As with generic methods, when a generic class is compiled, the compiler performs type checking on the class’s type parameters to ensure that they can be used with the code in the generic class. The constraints determine the operations that can be performed on the type parameters. The runtime system replaces the type parameters with the actual types at runtime. For class Stack (Fig. 22.5), no type constraint is specified, so the default type constraint, object, is used. The scope of a generic class’s type parameter is the entire class.

Now, let’s consider an application (Fig. 22.8) that uses the Stack generic class. Lines 13–14 declare variables of type Stack<double> (pronounced “Stack of double”) and Stack<int> (pronounced “Stack of int”). The types double and int are the Stack’s type arguments. The compiler replaces the type parameters in the generic class so that the compiler can perform type checking. Method Main instantiates objects doubleStack of size 5 (line 18) and intStack of size 10 (line 19), then calls methods TestPushDouble (lines 28–48), TestPopDouble (lines 51–73), TestPushInt (lines 76–96) and TestPopInt (lines 99–121) to manipulate the two Stacks in this example.

Example 22.8. Testing generic class Stack.

 1   // Fig. 22.8: StackTest.cs
 2   // Testing generic class Stack.
 3   using System;
 4
 5   class StackTest
 6   {
 7      // create arrays of doubles and ints
 8      private static double[] doubleElements =
 9         new double[]{ 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 };
10      private static int[] intElements =
11         new int[]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
12
13      private static Stack< double > doubleStack; // stack stores doubles
14      private static Stack< int > intStack; // stack stores int objects  
15
16      public static void Main( string[] args )
17      {
18         doubleStack = new Stack< double >( 5 ); // stack of doubles
19         intStack = new Stack< int >( 10 ); // stack of ints
20
21         TestPushDouble(); // push doubles onto doubleStack
22         TestPopDouble(); // pop doubles from doubleStack
23         TestPushInt(); // push ints onto intStack
24         TestPopInt(); // pop ints from intStack
25      } // end Main
26
27      // test Push method with doubleStack
28      private static void TestPushDouble()
29      {
30         // push elements onto stack
31         try
32         {
33            Console.WriteLine( "
Pushing elements onto doubleStack" );
34
35            // push elements onto stack
36            foreach ( var element in doubleElements )
37            {
38               Console.Write( "{0:F1} ", element );
39               doubleStack.Push( element ); // push onto doubleStack
40            } // end foreach
41         } // end try
42         catch ( FullStackException exception )
43         {
44            Console.Error.WriteLine();
45            Console.Error.WriteLine( "Message: " + exception.Message );
46            Console.Error.WriteLine( exception.StackTrace );
47         } // end catch
48      } // end method TestPushDouble
49
50      // test Pop method with doubleStack
51      private static void TestPopDouble()
52      {
53         // pop elements from stack
54         try
55         {
56            Console.WriteLine( "
Popping elements from doubleStack" );
57
58            double popValue; // store element removed from stack
59
60            // remove all elements from stack
61            while ( true )
62            {
63               popValue = doubleStack.Pop(); // pop from doubleStack
64               Console.Write( "{0:F1} ", popValue );
65            } // end while
66         } // end try
67         catch ( EmptyStackException exception )
68         {
69            Console.Error.WriteLine();
70            Console.Error.WriteLine( "Message: " + exception.Message );
71            Console.Error.WriteLine( exception.StackTrace );
72         } // end catch
73      } // end method TestPopDouble
74
75      // test Push method with intStack
76      private static void TestPushInt()
77      {
78         // push elements onto stack
79         try
80         {
81            Console.WriteLine( "
Pushing elements onto intStack" );
82
83            // push elements onto stack
84            foreach ( var element in intElements )
85            {
86               Console.Write( "{0} ", element );
87               intStack.Push( element ); // push onto intStack
88            } // end foreach
89         } // end try
90         catch ( FullStackException exception )
91         {
92            Console.Error.WriteLine();
93            Console.Error.WriteLine( "Message: " + exception.Message );
94            Console.Error.WriteLine( exception.StackTrace );
95         } // end catch
96      } // end method TestPushInt
97
98      // test Pop method with intStack
99      private static void TestPopInt()
100     {
101        // pop elements from stack
102        try
103        {
104           Console.WriteLine( "
Popping elements from intStack" );
105
106           int popValue; // store element removed from stack
107
108           // remove all elements from stack
109           while ( true )
110           {
111              popValue = intStack.Pop(); // pop from intStack
112              Console.Write( "{0} ", popValue );
113           } // end while
114        } // end try
115        catch ( EmptyStackException exception )
116        {
117           Console.Error.WriteLine();
118           Console.Error.WriteLine( "Message: " + exception.Message );
119           Console.Error.WriteLine( exception.StackTrace );
120        } // end catch
121     } // end method TestPopInt
122  } // end class StackTest
Pushing elements onto doubleStack
1.1 2.2 3.3 4.4 5.5 6.6
Message: Stack is full, cannot push 6.6
   at Stack`1.Push(T pushValue) in
      C:Examplesch22Fig22_05_08StackStackStack.cs:line 36
   at StackTest.TestPushDouble() in
      C:Examplesch22Fig22_05_08StackStackStackTest.cs:line 39
Popping elements from doubleStack
5.5 4.4 3.3 2.2 1.1

Message: Stack is empty, cannot pop
   at Stack`1.Pop() in
      C:Examplesch22Fig22_05_08StackStackStack.cs:line 47
   at StackTest.TestPopDouble() in
      C:Examplesch22Fig22_05_08StackStackStackTest.cs:line 63

Pushing elements onto intStack
1 2 3 4 5 6 7 8 9 10 11
Message: Stack is full, cannot push 11
   at Stack`1.Push(T pushValue) in
      C:Examplesch22Fig22_05_08StackStackStack.cs:line 36
   at StackTest.TestPushInt() in
      C:Examplesch22Fig22_05_08StackStackStackTest.cs:line 87

Popping elements from intStack
10 9 8 7 6 5 4 3 2 1
Message: Stack is empty, cannot pop
   at Stack`1.Pop() in
      C:Examplesch22Fig22_05_08StackStackStack.cs:line 47
   at StackTest.TestPopInt() in
      C:Examplesch22Fig22_05_08StackStackStackTest.cs:line 111

Method TestPushDouble (lines 28–48) invokes method Push to place the double values 1.1, 2.2, 3.3, 4.4 and 5.5 stored in array doubleElements onto doubleStack. The foreach statement terminates when the test program attempts to Push a sixth value onto doubleStack (which is full, because doubleStack can store only five elements). In this case, the method throws a FullStackException (Fig. 22.6) to indicate that the Stack is full. Lines 42–47 catch this exception and display the message and stack-trace information. The stack trace indicates the exception that occurred and shows that Stack method Push generated the exception at line 36 of the file Stack.cs (Fig. 22.5). The trace also shows that method Push was called by StackTest method TestPushDouble at line 39 of StackTest.cs. This information enables you to determine the methods that were on the method-call stack at the time that the exception occurred. Because the program catches the exception, the C# runtime environment considers the exception to have been handled, and the program can continue executing.

Method TestPopDouble (lines 51–73) invokes Stack method Pop in an infinite while loop to remove all the values from the stack. Note in the output that the values are popped off in last-in, first-out order—this, of course, is the defining characteristic of stacks. The while loop (lines 61–65) continues until the stack is empty. An EmptyStackException occurs when an attempt is made to pop from the empty stack. This causes the program to proceed to the catch block (lines 67–72) and handle the exception, so the program can continue executing. When the test program attempts to Pop a sixth value, the doubleStack is empty, so method Pop throws an EmptyStackException.

Method TestPushInt (lines 76–96) invokes Stack method Push to place values onto intStack until it’s full. Method TestPopInt (lines 99–121) invokes Stack method Pop to remove values from intStack until it’s empty. Once again, note that the values pop off in last-in, first-out order.

Creating Generic Methods to Test Class Stack< T >

Note that the code in methods TestPushDouble and TestPushInt is almost identical for pushing values onto a Stack<double> or a Stack<int>, respectively. Similarly the code in methods TestPopDouble and TestPopInt is almost identical for popping values from a Stack<double> or a Stack<int>, respectively. This presents another opportunity to use generic methods. Figure 22.9 declares generic method TestPush (lines 33–54) to perform the same tasks as TestPushDouble and TestPushInt in Fig. 22.8—that is, Push values onto a Stack<T>. Similarly, generic method TestPop (lines 57–79) performs the same tasks as TestPopDouble and TestPopInt in Fig. 22.8—that is, Pop values off a Stack<T>. Note that the output of Fig. 22.9 precisely matches the output of Fig. 22.8.

Example 22.9. Testing generic class Stack.

 1   // Fig. 22.9: StackTest.cs
 2   // Testing generic class Stack.
 3   using System;
 4   using System.Collections.Generic;
 5
 6   class StackTest
 7   {
 8      // create arrays of doubles and ints
 9      private static double[] doubleElements =
10         new double[] { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 };
11      private static int[] intElements =
12         new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
13
14      private static Stack< double > doubleStack; // stack stores doubles
15      private static Stack< int > intStack; // stack stores int objects
16
17      public static void Main( string[] args )
18      {
19         doubleStack = new Stack< double >( 5 ); // stack of doubles
20         intStack = new Stack< int >( 10 ); // stack of ints
21
22         // push doubles onto doubleStack
23         TestPush( "doubleStack", doubleStack, doubleElements );
24         // pop doubles from doubleStack
25         TestPop( "doubleStack", doubleStack );
26         // push ints onto intStack
27         TestPush( "intStack", intStack, intElements );
28         // pop ints from intStack
29         TestPop( "intStack", intStack );
30      } // end Main
31
32      // test Push method
33      private static void TestPush< T >( string name, Stack< T > stack,
34         IEnumerable< T > elements )                                   
35      {
36         // push elements onto stack
37         try
38         {
39            Console.WriteLine( "
Pushing elements onto " + name );
40
41            // push elements onto stack
42            foreach ( var element in elements )
43            {
44               Console.Write( "{0} ", element );
45               stack.Push( element ); // push onto stack
46            } // end foreach
47         } // end try
48         catch ( FullStackException exception )
49         {
50            Console.Error.WriteLine();
51            Console.Error.WriteLine( "Message: " + exception.Message );
52            Console.Error.WriteLine( exception.StackTrace );
53         } // end catch
54      } // end method TestPush
55
56      // test Pop method
57      private static void TestPop< T >( string name, Stack< T > stack )
58      {
59         // push elements onto stack
60         try
61         {
62            Console.WriteLine( "
Popping elements from " + name );
63
64            T popValue; // store element removed from stack
65
66            // remove all elements from stack
67            while ( true )
68            {
69               popValue = stack.Pop(); // pop from stack
70               Console.Write( "{0} ", popValue );
71            } // end while
72         } // end try
73         catch ( EmptyStackException exception )
74         {
75            Console.Error.WriteLine();
76            Console.Error.WriteLine( "Message: " + exception.Message );
77            Console.Error.WriteLine( exception.StackTrace );
78         } // end catch
79      } // end TestPop
80   } // end class StackTest
Pushing elements onto doubleStack
1.1 2.2 3.3 4.4 5.5 6.6
Message: Stack is full, cannot push 6.6
   at Stack`1.Push(T pushValue) in
      C:Examplesch22Fig22_09StackStackStack.cs:line 36
   at StackTest.TestPush[T](String name, Stack`1 stack, IEnumerable`1
      elements) in C:Examplesch22Fig22_09StackStackStackTest.cs:line 45

Popping elements from doubleStack
5.5 4.4 3.3 2.2 1.1

Message: Stack is empty, cannot pop
   at Stack`1.Pop() in
      C:Examplesch22Fig22_09StackStackStack.cs:line 47
   at StackTest.TestPop[T](String name, Stack`1 stack) in
      C:Examplesch22Fig22_09StackStackStackTest.cs:line 69

Pushing elements onto intStack
1 2 3 4 5 6 7 8 9 10 11
Message: Stack is full, cannot push 11
   at Stack`1.Push(T pushValue) in
      C:Examplesch22Fig22_09StackStackStack.cs:line 36
   at StackTest.TestPush[T](String name, Stack`1 stack, IEnumerable`1
      elements) in C:Examplesch22Fig22_09StackStackStackTest.cs:line 45

Popping elements from intStack
10 9 8 7 6 5 4 3 2 1
Message: Stack is empty, cannot pop
   at Stack`1.Pop() in
      C:Examplesch22Fig22_09StackStackStack.cs:line 47
   at StackTest.TestPop[T](String name, Stack`1 stack) in
      C:Examplesch22Fig22_09StackStackStackTest.cs:line 69

Method Main (lines 17–30) creates the Stack<double> (line 19) and Stack<int> (line 20) objects. Lines 23–29 invoke generic methods TestPush and TestPop to test the Stack objects.

Generic method TestPush (lines 33–54) uses type parameter T (specified at line 33) to represent the data type stored in the Stack. The generic method takes three arguments—a string that represents the name of the Stack object for output purposes, an object of type Stack<T> and an IEnumerable<T> that contains the elements that will be Pushed onto Stack<T>. Note that the compiler enforces consistency between the type of the Stack and the elements that will be pushed onto the Stack when Push is invoked, which is the type argument of the generic method call. Generic method TestPop (lines 57–79) takes two arguments—a string that represents the name of the Stack object for output purposes and an object of type Stack<T>.

Wrap-Up

This chapter introduced generics. We discussed how generics ensure compile-time type safety by checking for type mismatches at compile time. You learned that the compiler will allow generic code to compile only if all operations performed on the type parameters in the generic code are supported for all types that could be used with the generic code. You also learned how to declare generic methods and classes using type parameters. We demonstrated how to use a type constraint to specify the requirements for a type parameter—a key component of compile-time type safety. We discussed several kinds of type constraints, including reference-type constraints, value-type constraints, class constraints, interface constraints and constructor constraints. We also discussed how to implement multiple type constraints for a type parameter. Finally, we showed how generics improve code reuse. In the next chapter, we demonstrate the .NET Framework Class Library’s collection classes, interfaces and algorithms. Collection classes are pre-built data structures that you can reuse in your applications, saving you time.

Summary

Section 22.1 Introduction

  • Generic methods enable you to specify, with a single method declaration, a set of related methods.

  • Generic classes enable you to specify, with a single class declaration, a set of related classes.

  • Generic interfaces enable you to specify, with a single interface declaration, a set of related interfaces.

  • Generics provide compile-time type safety.

Section 22.2 Motivation for Generic Methods

  • Overloaded methods are often used to perform similar operations on different types of data.

  • When the compiler encounters a method call, it attempts to locate a method declaration that has the same method name and parameters that match the argument types in the method call.

Section 22.3 Generic-Method Implementation

  • If the operations performed by several overloaded methods are identical for each argument type, the overloaded methods can be more compactly and conveniently coded using a generic method.

  • You can write a single generic-method declaration that can be called at different times with arguments of different types. Based on the types of the arguments passed to the generic method, the compiler handles each method call appropriately.

  • All generic-method declarations have a type-parameter list delimited by angle brackets that follows the method’s name. Each type-parameter list contains one or more type parameters, separated by commas.

  • A type parameter is used in place of actual type names. The type parameters can be used to declare the return type, parameter types and local variable types in a generic-method declaration; the type parameters act as placeholders for type arguments that represent the types of data that will be passed to the generic method.

  • A generic method’s body is declared like that of any other method. The type-parameter names throughout the method declaration must match those declared in the type-parameter list.

  • A type parameter can be declared only once in the type-parameter list but can appear more than once in the method’s parameter list. Type-parameter names need not be unique among different generic methods.

  • When the compiler encounters a method call, it analyzes the set of methods (both nongeneric and generic) that might match the method call, looking for a method that best matches the call. If there are no matching methods, or if there’s more than one best match, the compiler generates an error.

  • You can use explicit type arguments to indicate the exact type that should be used to call a generic function. For example, the method call DisplayArray<int>(intArray); explicitly provides the type argument (int) that should be used to replace type parameter T in the DisplayArray method’s declaration.

  • For each variable declared with a type parameter, the compiler also determines whether the operations performed on such a variable are allowed for all types that the type parameter can assume.

Section 22.4 Type Constraints

  • Generic code is restricted to performing operations that are guaranteed to work for every possible type. Thus, an expression like variable1 < variable2 is not allowed unless the compiler can ensure that the operator < is provided for every type that will ever be used in the generic code. Similarly, you cannot call a method on a generic-type variable unless the compiler can ensure that all types that will ever be used in the generic code support that method.

  • It’s possible to compare two objects of the same type if that type implements the generic interface IComparable<T> (of namespace System), which declares method CompareTo.

  • IComparable<T> objects can be used with the sorting and searching methods of classes in the System.Collections.Generic namespace.

  • Simple types all implement interface IComparable<T>.

  • It’s the responsibility of the programmer who declares a type that implements IComparable<T> to declare method CompareTo such that it compares the contents of two objects of that type and returns the appropriate result.

  • You can restrict the types that can be used with a generic method or class to ensure that they meet certain requirements. This feature—known as a type constraint—restricts the type of the argument supplied to a particular type parameter. For example, the clause where T : IComparable<T> indicates that the type arguments must implement interface IComparable<T>. If no type constraint is specified, the default type constraint is object.

  • A class constraint indicates that the type argument must be an object of a specific base class or one of its subclasses.

  • An interface constraint indicates that the type argument’s class must implement a specific interface.

  • You can specify that the type argument must be a reference type or a value type by using the reference-type constraint (class) or the value-type constraint (struct), respectively.

  • You can specify a constructor constraint—new()—to indicate that the generic code can use operator new to create new objects of the type represented by the type parameter. If a type parameter is specified with a constructor constraint, the type argument’s class must provide public a parameterless or default constructor to ensure that objects of the class can be created without passing constructor arguments; otherwise, a compilation error occurs.

  • It’s possible to apply multiple constraints to a type parameter by providing a comma-separated list of constraints in the where clause.

  • If you have a class constraint, reference-type constraint or value-type constraint, it must be listed first—only one of these types of constraints can be used for each type parameter. Interface constraints (if any) are listed next. The constructor constraint is listed last (if there is one).

Section 22.5 Overloading Generic Methods

  • A generic method may be overloaded. All methods must contain a unique signature.

  • A generic method can be overloaded by nongeneric methods with the same method name. When the compiler encounters a method call, it searches for the method declaration that most precisely matches the method name and the argument types specified in the call.

Section 22.6 Generic Classes

  • A generic class provides a means for describing a class in a type-independent manner.

  • Once you have a generic class, you can use a simple, concise notation to indicate the actual type(s) that should be used in place of the class’s type parameter(s). At compilation time, the compiler ensures the type safety of your code, and the runtime system replaces type parameters with actual arguments to enable your client code to interact with the generic class.

  • A generic class declaration is similar to a nongeneric class declaration, except that the class name is followed by a type-parameter list and optional constraints on its type parameter.

  • As with generic methods, the type-parameter list of a generic class can have one or more type parameters separated by commas.

  • When a generic class is compiled, the compiler performs type checking on the class’s type parameters to ensure that they can be used with the code in the generic class. The constraints determine the operations that can be performed on the variables declared with type parameters.

Self-Review Exercises

22.1

State whether each of the following is true or false. If false, explain why.

  1. A generic method cannot have the same method name as a nongeneric method.

  2. All generic method declarations have a type-parameter list that immediately precedes the method name.

  3. A generic method can be overloaded by another generic method with the same method name but a different number of type parameters.

  4. A type parameter can be declared only once in the type-parameter list but can appear more than once in the method’s parameter list.

  5. Type-parameter names among different generic methods must be unique.

  6. The scope of a generic class’s type parameter is the entire class.

  7. A type parameter can have at most one interface constraint, but multiple class constraints.

22.1

  1. False. A generic method can be overloaded by nongeneric methods with the same or a different number of arguments.

  2. False. All generic method declarations have a type-parameter list that immediately follows the method’s name.

  3. True.

  4. True.

  5. False. Type-parameter names among different generic methods need not be unique.

  6. True.

  7. False. A type parameter can have at most one class constraint, but multiple interface constraints.

22.2

Fill in the blanks in each of the following:

  1. __________ enable you to specify, with a single method declaration, a set of related methods; __________ enable you to specify, with a single class declaration, a set of related classes.

  2. A type-parameter list is delimited by __________.

  3. The __________ of a generic method can be used to specify the types of the arguments to the method, to specify the return type of the method and to declare variables within the method.

  4. The statement “Stack<int> objectStack = new Stack<int>();” indicates that objectStack stores __________.

  5. In a generic class declaration, the class name is followed by a(n) __________.

  6. The __________ constraint requires that the type argument must have a public parameterless constructor.

22.2

  1. Generic methods, generic classes.

  2. angle brackets.

  3. type parameters.

  4. ints.

  5. type-parameter list.

  6. new.

Answers to Self-Review Exercises

Exercises

22.3

(Generic Notation) Explain the use of the following notation in a C# program:

public class Array<T>

22.4

(Overloading Generic Methods) How can generic methods be overloaded?

22.5

(Determining which Method to Call) The compiler performs a matching process to determine which method to call when a method is invoked. Under what circumstances does an attempt to make a match result in a compile-time error?

22.6

(What Does this Statement Do?) Explain why a C# program might use the statement

Array< Employee > workerlist = new Array< Employee >();

22.7

(Generic Linear Search Method) Write a generic method, Search, that implements the linear-search algorithm. Method Search should compare the search key with each element in the array until the search key is found or until the end of the array is reached. If the search key is found, return its location in the array; otherwise, return -1. Write a test application that inputs and searches an int array and a double array. Provide buttons that the user can click to randomly generate int and double values. Display the generated values in a TextBox, so the user knows what values they can search for [Hint: Use (T : IComparable< T >) in the where clause for method Search so that you can use method CompareTo to compare the search key to the elements in the array.]

22.8

(Overloading a Generic Method) Overload generic method DisplayArray of Fig. 22.3 so that it takes two additional int arguments: lowIndex and highIndex. A call to this method displays only the designated portion of the array. Validate lowIndex and highIndex. If either is out of range, or if highIndex is less than or equal to lowIndex, the overloaded DisplayArray method should throw an InvalidIndexException; otherwise, DisplayArray should return the number of elements displayed. Then modify Main to exercise both versions of DisplayArray on arrays intArray, doubleArray and charArray. Test all capabilities of both versions of DisplayArray.

22.9

(Overloading a Generic Method with a Non-Generic Method) Overload generic method DisplayArray of Fig. 22.3 with a nongeneric version that displays an array of strings in neat, tabular format, as shown in the sample output that follows:

Array stringArray contains:
one      two      three    four
five     six      seven    eight

22.10

(Generic Method IsEqualTo) Write a simple generic version of method IsEqualTo that compares its two arguments with the Equals method, and returns true if they’re equal and false otherwise. Use this generic method in a program that calls IsEqualTo with a variety of simple types, such as object or int. What result do you get when you attempt to run this program?

22.11

(Generic Class Pair) Write a generic class Pair which has two type parameters, F and S, representing the type of the first and second element of the pair, respectively. Add properties for the first and second elements of the pair. [Hint: The class header should be public class Pair<F, S>.]

22.12

(Generic Classes TreeNode and Tree) Convert classes TreeNode and Tree from Fig. 21.20 into generic classes. To insert an object in a Tree, the object must be compared to the objects in existing TreeNodes. For this reason, classes TreeNode and Tree should specify IComparable<T> as the interface constraint of each class’s type parameter. After modifying classes TreeNode and Tree, write a test application that creates three Tree objects—one that stores ints, one that stores doubles and one that stores strings. Insert 10 values into each tree. Then output the preorder, inorder and postorder traversals for each Tree.

22.13

(Generic Method TestTree) Modify your test program from Exercise 22.12 to use generic method TestTree to test the three Tree objects. The method should be called three times—once for each Tree object.

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

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