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 20.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 20.11.) Type parameter T is used throughout the Stack class declaration (Fig. 20.5) to represent the element type. Class Stack declares variable elements as an array of type T (line 8). This array (created at line 25) will store the Stack’s elements. [Note: This example implements a Stack as an array. As you’ve seen in Chapter 19, Stacks also are commonly implemented as constrained versio/ns of linked lists.]
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. For value-types, the compiler generates a custom version of the class for each unique value type used to create a new Stack object, and for reference types, the compiler generates a single additional custom Stack. The constraints determine the operations that can be performed on the type parameters. For reference types, the runtime system replaces the type parameters with the actual types. For class Stack, 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.
Stack Constructors
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–27) validates the stackSize argument and creates an array of the specified stackSize (if it’s greater than 0) or throws an exception, otherwise.
Stack Method Push
Method Push (lines 31–41) first determines whether an attempt is being made to push an element onto a full Stack. If so, line 35–36 throw a FullStackException (declared in Fig. 20.6). If the Stack is not full, line 39 increments the top counter to indicate the new top position, and line 40 places the argument in that location of array elements.
Stack Method Pop
Method Pop (lines 45–54) first determines whether an attempt is being made to pop an element from an empty Stack. If so, line 49 throws an EmptyStackException (declared in Fig. 20.7). Otherwise, line 52 decrements the top counter to indicate the new top position, and line 53 returns the original top element of the Stack.
Classes FullStackException and EmptyStackException
Classes FullStackException (Fig. 20.6) and EmptyStackException (Fig. 20.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.
Demonstrating Class Stack
Now, let’s consider an app (Fig. 20.8) that uses our generic Stack 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 and performs type checking. Method Main instantiates objects doubleStack of size 5 (line 18) and intStack of size 10 (line 19), then calls methods TestPushDouble (declared in lines 28–47), TestPopDouble (declared in lines 50–71), TestPushInt (declared in lines 74–93) and TestPopInt (declared in lines 96–117) to manipulate the two Stacks in this example.
Method TestPushDouble
Method TestPushDouble (lines 28–47) 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. 20.6) to indicate that the Stack is full. Lines 42–46 of Fig. 20.8 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 35 of the file Stack.cs (Fig. 20.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
Method TestPopDouble (Fig. 20.8, lines 50–71) 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 60–64) continues until the stack is empty. An EmptyStack-Exception occurs when an attempt is made to pop from the empty stack. This causes the program to proceed to the catch block (lines 66–70) 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.
Methods TestPushInt and TestPopInt
Method TestPushInt (lines 74–93) invokes Stack method Push to place values onto int-Stack until it’s full. Method TestPopInt (lines 96–117) invokes Stack method Pop to remove values from intStack until it’s empty. Again, values pop in last-in, first-out order.
Creating Generic Methods to Test Class Stack<T>
Note that the code in methods TestPushDouble and TestPushInt is virtually identical for pushing values onto Stacks. Similarly the code in methods TestPopDouble and TestPopInt is virtually identical for popping values from Stacks. This presents another opportunity to use generic methods. Figure 20.9 declares generic method TestPush (lines 33–53) to perform the same tasks as TestPushDouble and TestPushInt in Fig. 20.8—that is, Push values onto a Stack<T>. Similarly, generic method TestPop (lines 56–77) performs the same tasks as TestPopDouble and TestPopInt in Fig. 20.8—that is, Pop values off a Stack<T>.
Method Main (Fig. 20.9, 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–53) 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>. 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 56–77) takes two arguments—a string that represents the name of the Stack object for output purposes and an object of type Stack<T>.