Essential C# 2.0

C# without Generics

I will begin the discussion of generics by examining a class that does not use generics. The class is System.Collections.Stack, and its purpose is to represent a collection of objects such that the last item to be added to the collection is the first item retrieved from the collection (called last in, first out, or 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 Pop() and Push() methods on the stack class appear in Listing 11.1.

Listing 11.1. The Stack Definition Using a Data Type Object

public class Stack { public virtual object Pop(); public virtual void Push(object obj); // ... }

Programs frequently use stack type collections to facilitate multiple undo operations. For example, Listing 11.2 uses the stack class for undo operations within a program which simulates the Etch A Sketch game.

Listing 11.2. Supporting Undo in a Program Similar to the Etch A Sketch Game

using System; using System.Collections; class Program { // ... public void Sketch() { Stack path = new Stack(); Cell currentPosition; ConsoleKeyInfo key; // New with C# 2.0 do { // Etch in the direction indicated by the // arrow keys that the user enters. key = Move(); switch (key.Key) { case ConsoleKey.Z: // Undo the previous Move. if (path.Count >= 1) { currentPosition = (Cell)path.Pop(); Console.SetCursorPosition( currentPosition.X, currentPosition.Y); Undo(); } break ; case ConsoleKey.DownArrow: case ConsoleKey.UpArrow: case ConsoleKey.LeftArrow: case ConsoleKey.RightArrow: // SaveState() currentPosition = new Cell( Console.CursorLeft, Console.CursorTop); path.Push(currentPosition); break ; default: Console.Beep(); // New with C#2.0 break ; } } while (key.Key != ConsoleKey.X); // Use X to quit. } } public struct Cell { readonly public int X; readonly public int Y; public Cell(int x, int y) { X = x; Y = y; } }

The results of Listing 11.2 appear in Output 11.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 Ctrl+Z), then 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(). (Note that this code uses some CLR 2.0-specific console functions as well.)

Output 11.1.

Although the code is functional, there is a fundamental drawback in the System.Collections.Stack class. As shown in Listing 11.1, the Stack class collects variables of type object. Because every object in the 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 in some scenarios, it is desirable.

Furthermore, when retrieving the data from the stack using the Pop() method, you must cast the return value to a Cell. But if the value returned from the Pop() method is not a Cell type object, an exception is thrown. You can test the data type, but splattering such checks builds complexity. The fundamental problem with creating classes that can work with multiple data types without generics is that they must use a common base type, generally object data.

Using value types, such as a struct or 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. While the widening operation (cast to a base class) for a reference type has a negligible performance impact, the box operation for a value type introduces nontrivial overhead.

To change the Stack class to enforce storage on a particular data type using the preceding C# programming constructs, you must create a specialized stack class, as in Listing 11.3.

Listing 11.3. Defining a Specialized Stack Class

public class CellStack { public virtual Cell Pop(); public virtual void Push(Cell cell); // ... }

Because CellStack can store only objects of type Cell, this solution requires a custom implementation of the stack methods, which is less than ideal.

Beginner Topic: Another Example: Nullable Value Types

Chapter 2 introduced the capability of declaring variables that could contain null by using the nullable modifier, ?, when declaring the value type variable. C# only began supporting this 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 11.4.

Listing 11.4. Declaring Versions of Various Value Types That Store null

struct NullableInt { public int Value {...} public bool HasValue {...} ... } struct NullableGuid { public Guid Value {...} public bool HasValue {...} ... } ...

Listing 11.4 shows implementations for only NullableInt and NullableGuid. If a program required additional nullable value types, you would have to create a copy with the additional value type. If the nullable implementation changed (if it supported a cast from a null to the nullable type, for example), you would have to add the modification to all of the nullable type declarations.

The second option was to declare a nullable type that contains a Value property of type object, as shown in Listing 11.5.

Listing 11.5. Declaring a Nullable Type That Contains a Value Property of Type object

struct Nullable { public object Value {...} public bool HasValue {...} ... }

Although this option requires only one implementation of a nullable type, the runtime always boxes value types when setting the Value property. Furthermore, calls to retrieve data from Nullable.Value will not be strongly typed, so retrieving the value type will require a cast operation, which is potentially invalid at runtime.

Neither option is particularly attractive. To deal with dilemmas like this, C# 2.0 includes the concept of generics. In fact, the nullable modifier, ?, uses generics internally.

Категории