C# Generics
we presented data structures that stored and manipulated object references. 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. Also, 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 25), 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 one of C#'s newest features-generics-which provides 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. For more information, see the C# language specification.]
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 same 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 same 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 and inheritance. Chapter 27, Collections, discusses the .NET Framework's generic and non-generic 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.
Overloaded methods are often used to perform similar operations on different types of data. To motivate generic methods, let's begin with an example (Fig. 26.1) that contains three overloaded PrintArray 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 will reimplement this program more concisely and elegantly using a single generic method.
Fig. 26.1 Displaying arrays of different types using overloaded methods. 1 // Fig. 25.1: OverloadedMethods.cs
2 // Using overloaded methods to print 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 PrintArray( intArray ); // pass an int array argument
16 Console.WriteLine( "Array doubleArray contains:" );
17 PrintArray( doubleArray ); // pass a double array argument
18 Console.WriteLine( "Array charArray contains:" );
19 PrintArray( charArray ); // pass a char array argument
20 } // end Main
21
22 // output int array
23 static void PrintArray( int[] inputArray )
24 {
25 foreach ( int element in inputArray )
26 Console.Write( element + " " );
27
28 Console.WriteLine( "\n" );
29 } // end method PrintArray
30
31 // output double array
32 static void PrintArray( double[] inputArray )
33 {
34 foreach ( double element in inputArray )
35 Console.Write( element + " " );
36
37 Console.WriteLine( "\n" );
38 } // end method PrintArray
39
40 // output char array
41 static void PrintArray( char[] inputArray )
42 {
43 foreach ( char element in inputArray )
44 Console.Write( element + " " );
45
46 Console.WriteLine( "\n" );
47 } // end method PrintArray
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 PrintArray call exactly matches one of the PrintArray method declarations. For example, line 15 calls PrintArray with intArray as its argument. At compile time, the compiler determines argument intArray's type (i.e., int[]), attempts to locate a method named PrintArray 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 PrintArray call at line 17, it determines argument doubleArray's type (i.e., double[]), then attempts to locate a method named PrintArray 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 PrintArray call at line 19, it determines argument charArray's type (i.e., char[]), then attempts to locate a method named PrintArray that specifies a single char[] parameter (which it finds at lines 41-47) and sets up a call to that method.
Study each PrintArray 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-we chose E to represent the "element" type-then all three methods would look like the one in Fig. 26.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 PrintArray method that can display the elements of any array. The method in Fig. 26.2 will not compile, because its syntax is not correct. We declare a generic PrintArray method with the proper syntax in Fig. 26.3.
Fig. 26.2 PrintArray method in which actual typenames are replaced by convention with the generic name E 1 static void PrintArray( E[] inputArray )
2 {
3 foreach( E element in inputArray )
4 Console.Write( element + " " );
5
6 Console.WriteLine( "\n" );
7 } // end method PrintArray
No comments:
Post a Comment