12

Generics

As your projects become more sophisticated, you will need a better way to reuse and customize existing software. To facilitate code reuse, especially the reuse of algorithms, C# includes a feature called generics.

Generics are lexically like generic types in Java and templates in C++. In all three languages, these features enable the implementation of algorithms and patterns once, rather than requiring separate implementations for each type that the algorithm or pattern operates on. However, C# generics are very different from both Java generics and C++ templates in the details of their implementation and impact on the type system of their respective languages. Just as methods are powerful because they can take arguments, so types and methods that take type arguments have significantly more functionality.

Generics were added to the runtime and C# in version 2.0.

C# without Generics

We begin the discussion of generics by examining a class that does not use generics. This class, System.Collections.Stack, represents a collection of objects such that the last item to be added to the collection is the first item retrieved from the collection (last in, first out [LIFO]). Push() and Pop(), the two main methods of the Stack class, add items to the stack and remove them from the stack, respectively. The declarations for the methods on the Stack class appear in Listing 12.1.

Listing 12.1: The System.Collections.Stack Method Signatures
1. public class Stack
2. {
3.     public virtual object Pop()
4.     {
5.         // ...
6.     }
7.  
8.     public virtual void Push(object obj)
9.     {
10.         // ...
11.     }
12.     // ...
13. }

Programs frequently use stack type collections to facilitate multiple undo operations. For example, Listing 12.2, with results shown in Output 12.1, uses the System.Collections.Stack class for undo operations within a program that simulates an Etch A Sketch toy.

Listing 12.2: Supporting Undo in a Program Similar to an Etch A Sketch Toy
1. using System.Collections.Generic;
2.  
3. public class Program
4. {
5.     // ...
6.  
7.     public static void Sketch()
8.     {
9.         Stack<Cell> path = new();
10.         Cell currentPosition;
11.         ConsoleKeyInfo key;
12.         // ...
13.  
14.         do
15.         {
16.             // Etch in the direction indicated by the
17.             // arrow keys that the user enters
18.             key = Move();
19.  
20.             switch(key.Key)
21.             {
22.                 case ConsoleKey.Z:
23.                     // Undo the previous Move
24.                     if(path.Count >= 1)
25.                     {
26.                         currentPosition = (Cell)path.Pop();
27.                         Console.SetCursorPosition(
28.                             currentPosition.X, currentPosition.Y);
29.                         // ...
30.                         Undo();
31.                     }
32.                     break;
33.                 case ConsoleKey.DownArrow:
34.                     // ...
35.                 case ConsoleKey.UpArrow:
36.                     // ...
37.                 case ConsoleKey.LeftArrow:
38.                     // ...
39.                 case ConsoleKey.RightArrow:
40.                     // SaveState()
41.                     if(Console.CursorLeft < Console.WindowWidth - 2)
42.                     {
43.                         currentPosition = new Cell(
44.                             Console.CursorLeft + 1, Console.CursorTop);
45.                     }
46.                     path.Push(currentPosition);
47.                     // ...
48.                     break;
49.  
50.                 default:
51.                     Console.Beep();
52.                     break;
53.             }
54.         }
55.         while(key.Key != ConsoleKey.X);  // Use X to quit.
56.     }
57.     // ...
58. }
59.  
60. public struct Cell
61. {
62.     public int X { get; }
63.     public int Y { get; }
64.  
65.     public Cell(int x, int y)
66.     {
67.         X = x;
68.         Y = y;
69.     }
70. }
Output 12.1

Using the variable path, which is declared as a System.Collections.Stack, you save the previous move by passing a custom type, Cell, into the Stack.Push() method using path.Push(currentPosition). If the user enters a Z (or presses Ctrl+Z), you undo the previous move by retrieving it from the stack using a Pop() method, setting the cursor position to be the previous position, and calling Undo().

Although this code is functional, the System.Collections.Stack class has a fundamental shortcoming. As shown in Listing 12.1, the Stack class collects values of type object. Because every object in the Common Language Runtime (CLR) derives from object, Stack provides no validation that the elements you place into it are homogenous or are of the intended type. For example, instead of passing currentPosition, you can pass a string in which X and Y are concatenated with a decimal point between them. However, the compiler must allow the inconsistent data types because the stack class is written to take any object, regardless of its more specific type.

Furthermore, when retrieving the data from the stack using the Pop() method, you must cast the return value to a Cell. But if the type of the value returned from the Pop() method is not Cell, an exception is thrown. By deferring type checking until runtime by using a cast, you make the program more brittle. The fundamental problem with creating classes that can work with multiple data types without generics is that they must work with a common base class (or interface), usually object.

Using value types, such as a struct or an integer, with classes that use object exacerbates the problem. If you pass a value type to the Stack.Push() method, for example, the runtime automatically boxes it. Similarly, when you retrieve a value type, you need to explicitly unbox the data and cast the object reference you obtain from the Pop() method into a value type. Casting a reference type to a base class or interface has a negligible performance impact, but the box operation for a value type introduces more overhead, because it must allocate memory, copy the value, and then later garbage-collect that memory.

C# is a language that encourages type safety: The language is designed so that many type errors, such as assigning an integer to a variable of type string, can be caught at compile time. The fundamental problem is that the Stack class is not as type-safe as one expects a C# program to be. To change the Stack class to enforce type safety by restricting the contents of the stack to be a particular data type (without using generic types), you must create a specialized stack class, as in Listing 12.3.

Listing 12.3: Defining a Specialized Stack Class
1. public class CellStack
2. {
3.     public virtual Cell Pop() { return new Cell(); } // would return
4.                                                      // that last cell added and remove it from the list
5.     public virtual void Push(Cell cell) { }
6.     // ...
7. }

Because CellStack can store only objects of type Cell, this solution requires a custom implementation of the stack methods, which is less than ideal. Implementing a type-safe stack of integers would require yet another custom implementation; each implementation would look remarkably like every other one. There would be lots of duplicated, redundant code.

Beginner Topic
Another Example: Nullable Value Types

Chapter 3 introduced the capability of declaring variables that could contain null by using the nullable modifier, ?, when declaring a value type variable. C# began supporting this functionality only in version 2.0, because the right implementation required generics. Prior to the introduction of generics, programmers faced essentially two options.

The first option was to declare a nullable data type for each value type that needs to handle null values, as shown in Listing 12.4.

Listing 12.4: Declaring Versions of Various Value Types That Store null
1. struct NullableInt
2. {
3.     /// <summary>
4.     /// Provides the value when HasValue returns true
5.     /// </summary>
6.     public int Value { getprivate set; }
7.  
8.     /// <summary>
9.     /// Indicates whether there is a value or whether
10.     /// the value is "null"
11.     /// </summary>
12.     public bool HasValue { getprivate set; }
13.  
14.     // ...
15. }
16.  
17. struct NullableGuid
18. {
19.     /// <summary>
20.     /// Provides the value when HasValue returns true
21.     /// </summary>
22.     public Guid Value { getprivate set; }
23.  
24.     /// <summary>
25.     /// Indicates whether there is a value or whether
26.     /// the value is "null"
27.     /// </summary>
28.     public bool HasValue { getprivate set; }
29.  
30.     // ...
31. }
32.  
33. // ...

Listing 12.4 shows possible implementations of NullableInt and NullableGuid. If a program required additional nullable value types, you would have to create yet another struct with the properties modified to use the desired value type. Any improvement of the implementation (e.g., adding a user-defined implicit conversion from the underlying type to the nullable type) would require modifying all the nullable type declarations.

An alternative strategy for implementing a nullable type without generics is to declare a single type with a Value property of type object, as shown in Listing 12.5.

Listing 12.5: Declaring a Nullable Type That Contains a Value Property of Type object
1. struct Nullable
2. {
3.     /// <summary>
4.     /// Provides the value when HasValue returns true.
5.     /// </summary>
6.     public object Value { getprivate set; }
7.  
8.     /// <summary>
9.     /// Indicates whether there is a value or whether
10.     /// the value is "null"
11.     /// </summary>
12.     public bool HasValue { getprivate set; }
13.  
14.     // ...
15. }

Although this option requires only one implementation of a nullable type, the runtime always boxes value types when setting the Value property. Furthermore, retrieving the underlying value from the Value property requires a cast operation, which might potentially be invalid at runtime.

Neither option is particularly attractive. To eliminate this problem, generics1 were added to C#. (And, in fact, nullable types are implemented as the generic type Nullable<T>.)

________________________________________

1. Introduced in C# 2.0.
{{ snackbarMessage }}
;