Chapter 19. Generics

What Are Generics?

With the language constructs you've learned so far, you can build powerful objects of many different types. You do this mostly by declaring classes that encapsulate the behavior you want, and then creating instances of those classes.

All the types used in the class declarations so far have been specific types—either programmer-defined, or supplied by the language or the BCL. There are times, however, when a class would be more useful if you could "distill" or "refactor" out its actions and apply them not just to the data types for which they are coded, but for other types as well.

Generics allow you to do just that. You can refactor your code and add an additional layer of abstraction so that, for certain kinds of code, the data types are not hard-coded. This is particularly designed for cases in which there are multiple sections of code performing the same instructions, but on different data types.

That might sound pretty abstract, so we'll start with an example that should make things clearer.

A Stack Example

Suppose first that you have created the following code, which declares a class called MyIntStack, which implements a stack of ints. It allows you to push ints onto the stack and pop them off.

A Stack Example

Suppose now that you would like the same functionality for values of type float. There are several ways you could achieve this. One way is to perform the following steps to produce the subsequent code:

  • Cut and paste the code for class MyIntStack.

  • Change the class name to MyFloatStack.

  • Change the appropriate int declarations to float declarations throughout the class declaration.

A Stack Example

This method certainly works, but it's error-prone, and has the following drawbacks:

  • You need to inspect every part of the class carefully to determine which type declarations need to be changed, and which should be left alone.

  • You need to repeat the process for each new type of stack class you need (long, double, string, etc.).

  • After the process, you end up with multiple copies of nearly identical code, taking up additional space.

  • Debugging and maintaining the parallel implementations is inelegant and error-prone.

Generics in C#

With C# 2.0, Microsoft introduced the generics features, which offer more elegant ways of using a set of code with more than one type. Generics allow you to declare type-parameterized code, which you can instantiate with different types. What this means is that you can write the code with "placeholders for types," and then supply the actual types when you create an instance of the class.

By this point in the text, you should be very familiar with the concept that a type is not an object, but a template for an object. In the same way, a generic type is not a type, but a template for a type. Figure 19-1 illustrates this point.

Generic types are templates for types.

Figure 19-1. Generic types are templates for types.

C# provides five kinds of generics: classes, structs, interfaces, delegates, and methods. Notice that the first four are types, and methods are members.

Figure 19-2 shows how generic types fit in with the other types covered.

Generics and user-defined types

Figure 19-2. Generics and user-defined types

Continuing with the Stack Example

In the stack example, with classes MyIntStack and MyFloatStack, the bodies of the declarations of the classes are identical except at the positions dealing with the type of the value held by the stack.

  • In MyIntStack, these positions are occupied by type int.

  • In MyFloatStack, they are occupied by float.

You can create a generic class from MyIntStack by doing the following:

  • Take the MyIntStack class declaration, and instead of substituting float for int, substitute the placeholder T.

  • Change the class name to MyStack.

  • Place the string <T> after the class name.

The result is the following generic class declaration. The string consisting of the angle brackets with the T means that T is a placeholder for a type. (It doesn't have to be the letter T—it can be any identifier.) Everywhere throughout the body of the class declaration where T is located, an actual type will need to be substituted by the compiler.

Continuing with the Stack Example

Generic Classes

Now that you've seen a generic class, let's look at generic classes in more detail and see how they are created and used.

When you are creating and using your own regular, non-generic classes, there are two steps in the process: declaring the class and creating instances of the class. But generic classes are not actual classes, but templates for classes—so you must first construct actual class types from them. You can then create references and instances from these constructed class types.

Figure 19-3 illustrates the process at a high level. If it's not all completely clear yet, don't worry—we'll cover each part in the following sections.

  1. Declare a class, using placeholders for some of the types.

  2. Provide actual types to substitute in for the placeholders. This gives you an actual class definition, with all the "blanks" filled in.

  3. Create instances from the "filled-in" class definition.

Creating instances from a generic type

Figure 19-3. Creating instances from a generic type

Declaring a Generic Class

Declaring a simple generic class is much like declaring a regular class, with the following differences:

  • Place a matching set of angle brackets after the class name.

  • Between the angle brackets, place a comma-separated list of the placeholder strings that represent the types, to be supplied on demand. These are called type parameters.

  • Use the type parameters throughout the body of the declaration of the generic class to represent the types that should be substituted in.

For example, the following code declares a generic class called SomeClass. The type parameters are listed between the angle brackets, and then used throughout the body of the declaration as if they were real types.

Declaring a Generic Class

There is no special keyword that flags a generic class declaration. The presence of the type parameter list, demarcated with angle brackets, distinguishes a generic class declaration from a regular class declaration.

Creating a Constructed Type

You cannot create class objects directly from a generic class. First, you need to tell the compiler what actual types should be substituted for the placeholders (the type parameters). The compiler takes those actual types and creates a template from which it creates actual class objects.

To construct a class type from a generic class, list the class name and supply real types between the angle brackets, in place of the type parameters. The real types being substituted for the type parameters are called type arguments.

Creating a Constructed Type

The compiler takes the type arguments and substitutes them for their corresponding type parameters throughout the body of the generic class, producing the constructed type—from which actual class instances are created.

Figure 19-4 shows the declaration of generic class SomeClass on the left. On the right, it shows the constructed class created by using the type arguments short and int.

Supplying type arguments for all the type parameters of a generic class produces a constructed class from which actual class objects can be created.

Figure 19-4. Supplying type arguments for all the type parameters of a generic class produces a constructed class from which actual class objects can be created.

Figure 19-5 illustrates the difference between type parameters and type arguments.

  • Generic class declarations have type parameters.

  • Type arguments are the actual types you supply when creating a constructed type.

Type parameters versus type arguments

Figure 19-5. Type parameters versus type arguments

Creating Variables and Instances

A constructed class type is used just like a regular type in creating references and instances.

For example, the following code shows the creation of two class objects.

  • The first line shows the creation of an object from a regular, non-generic class. This is a form that you should be completely familiar with by now.

  • The second line of code shows the creation of an object from generic class SomeClass, instantiated with types short and int. The form is exactly analogous to the line above it, with the constructed class forms in place of a regular class name.

  • The third line is the same semantically as the second line, but rather than listing the constructed type on both sides of the equals sign, it uses the var keyword to make the compiler use type inference.

Creating Variables and Instances

As with non-generic classes, the reference and the instance can be created separately, as shown in Figure 19-6. The figure also shows that what is going on in memory is the same as for a non-generic class.

  • The first line below the generic class declaration allocates a reference in the stack for variable myInst. Its value is null.

  • The second line allocates an instance in the heap, and assigns its reference to the variable.

Using a constructed type to create a reference and an instance

Figure 19-6. Using a constructed type to create a reference and an instance

Many different class types can be constructed from the same generic class. Each one is a separate class type, just as if it had its own separate non-generic class declaration.

For example, the following code shows the creation of two types from generic class SomeClass. The code is illustrated in Figure 19-7.

  • One type is constructed with types short and int.

  • The other is constructed with types int and long.

class SomeClass< T1, T2 >                            // Generic class
{
   ...
}

class Program
{
   static void Main()
   {
      var first  =  new SomeClass<short, int >();    // Constructed type
      var second =  new SomeClass<int,   long>();    // Constructed type

         ...
Two constructed classes created from a generic class

Figure 19-7. Two constructed classes created from a generic class

The Stack Example Using Generics

The following code shows the stack example implemented using generics. Method Main defines two variables: stackInt and stackString. The two constructed types are created using int and string as the type arguments.

class MyStack<T>
   {
      T[] StackArray;
      int StackPointer = 0;

      public void Push(T x) {
         if ( !IsStackFull )
            StackArray[StackPointer++] = x;
      }

      public T Pop() {
         return ( !IsStackEmpty )
            ? StackArray[--StackPointer]
            : StackArray[0];
      }

      const int MaxStack = 10;
      bool IsStackFull  { get{ return StackPointer >= MaxStack; } }
      bool IsStackEmpty { get{ return StackPointer <= 0; } }

      public MyStack() {
         StackArray = new T[MaxStack];
      }

      public void Print() {
         for (int i = StackPointer −1; i >= 0 ; i--)
            Console.WriteLine("   Value: {0}", StackArray[i]);
      }
   }
class Program {
      static void Main() {
          var    stackInt = new MyStack<int>();
          var stackString = new MyStack<string>();

          stackInt.Push(3); stackInt.Push(5); stackInt.Push(7);
          stackInt.Print();
          stackString.Push("Generics are great!");
          stackString.Push("Hi there!  ");
          stackString.Print();
       }
   }

This code produces the following output:

Value: 7
   Value: 5
   Value: 3
   Value: Hi there!
   Value: Generics are great!

Comparing the Generic and Non-Generic Stack

Table 19-1 summarizes some of the differences between the initial non-generic version of the stack and the final generic version of the stack. Figure 19-8 illustrates some of these differences.

Table 19-1. Differences Between the Non-Generic and Generic Stacks

 

Non-Generic

Generic

Source Code Size

Larger: You need a new implementation for each type.

Smaller: You only need one implementation regardless of the number of constructed types.

Executable Size

The compiled version of each stack will be present, regardless of whether it is used.

Only types for which there is a constructed type are present in the executable.

Ease of Writing

Easier to write.

Harder to write.

Difficulty to Maintain

More error-prone to maintain, since all changes need to be applied for each applicable type.

Easier to maintain, because modifications are only needed in one place.

Non-generic stack versus generic stack

Figure 19-8. Non-generic stack versus generic stack

Constraints on Type Parameters

In the generic stack example, the stack did not do anything with the items it contained other than store them and pop them. It did not try to add them, compare them, or do anything else that would require using operations of the items themselves. There's good reason for that. Since the generic stack doesn't know the type of the items it will be storing, it cannot know what members these types implement.

All C# objects, however, are ultimately derived from class object, so the one thing the stack can be sure of about the items it is storing is that they implement the members of class object. These include methods ToString, Equals, and GetType. Other than that, it can't know what members are available.

As long as your code does not access the objects of the types it handles (or as long as it sticks to the members of type object), your generic class can handle any type. Type parameters that meet this constraint are called unbounded type parameters. If, however, your code tries to use any other members, the compiler will produce an error message.

For example, the following code declares a class called Simple with a method called LessThan that takes two generic type variables. LessThan attempts to return the result of using the less-than operator. But not all classes implement the less-than operator, so the compiler produces an error message.

class Simple<T>
   {
      static public bool LessThan(T i1, T i2)
      {
         return i1 < i2;                      // Error
      }
      ...
   }

To make generics more useful, you need to be able to supply additional information to the compiler about what kinds of types are acceptable as arguments. These additional bits of information are called constraints. Only arguments that meet the constraints can be substituted for the type parameters.

Where Clauses

Constraints are listed as where clauses.

  • Each type parameter that has constraints has its own where clause.

  • If a parameter has multiple constraints, they are listed in the where clause, separated by commas.

The syntax of a where clause is the following:

Where Clauses

The important points about where clauses are the following:

  • They are listed after the closing angle bracket of the type parameter list.

  • They are not separated by commas, or any other token.

  • They can be listed in any order.

  • The token where is a contextual keyword, so you can use it in other contexts.

For example, the following generic class has three type parameters. T1 is unbounded. For T2, only classes of type Customer, or classes derived from Customer, can be used as type arguments. For T3, only classes that implement interface IComparable can be used as type arguments.

Where Clauses

Constraint Types and Order

There are five types of constraints. These are listed in Table 19-2.

Table 19-2. Types of Constraints

Constraint Type

Description

ClassName

Only classes of this type, or classes derived from it, can be used as the type argument.

class

Any reference type, including classes, arrays, delegates, and interfaces, can be used as the type argument.

struct

Any value type can be used as the type argument.

InterfaceName

Only this interface, or types that implement this interface, can be used as the type argument.

new()

Any type with a parameterless public constructor can be used as the type argument. This is called the constructor constraint.

The where clauses can be listed in any order. The constraints in a where clause, however, must be placed in a particular order, as shown in Figure 19-9.

  • There can be at most one primary constraint, and if there is one, it must be listed first.

  • There can be any number of InterfaceName constraints.

  • If the constructor constraint is present, it must be listed last.

If a type parameter has multiple constraints, they must be in this order.

Figure 19-9. If a type parameter has multiple constraints, they must be in this order.

The following declarations show examples of where clauses:

class SortedList<S>
            where S: IComparable<S> { ... }

   class LinkedList<M,N>
            where M : IComparable<M>
            where N : ICloneable    { ... }

   class MyDictionary<KeyType, ValueType>
            where KeyType : IEnumerable,
            new()                   { ... }

Generic Structs

Like generic classes, generic structs can have type parameters and constraints. The rules and conditions for generic structs are the same as those for generic classes.

For example, the following code declares a generic struct called PieceOfData, which stores and retrieves a piece of data, the type of which is determined when the type is constructed. Main creates objects of two constructed types—one using int and the other using string.

Generic Structs

This code produces the following output:

intData    = 10
stringData = Hi there.

Generic Interfaces

Generic interfaces allow you to write interfaces where the parameters and return types of interface members are generic type parameters. Generic interface declarations are similar to non-generic interface declarations, but have the type parameter list in angle brackets after the interface name.

For example, the following code declares a generic interface called IMyIfc.

  • Generic class Simple implements the generic interface.

  • Main instantiates two objects of the generic class: one with type int, and the other with type string.

Generic Interfaces

This code produces the following output:

5
Hi there.

An Example Using Generic Interfaces

The following example illustrates two additional capabilities of generic interfaces:

  • Like other generics, instances of a generic interface instantiated with different type parameters are different interfaces.

  • You can implement a generic interface in a non-generic type.

For example, the following code is similar to the last example, but in this case, Simple is a non-generic class that implements a generic interface. In fact, it implements two instances of IMyIfc. One instance is instantiated with type int, and the other with type string.

An Example Using Generic Interfaces

This code produces the following output:

5
Hi there.

Generic Interface Implementations Must Be Unique

When implementing an interface in a generic type, there must be no possible combination of type arguments that would create a duplicate interface in the type.

For example, in the following code, class Simple uses two instantiations of interface IMyIfc.

  • The first one is a constructed type, instantiated with type int.

  • The second one has a type parameter rather than an argument.

This causes a conflict because if int is used as the type argument to replace S in the second interface, then Simple would have two interfaces of the same type—which is not allowed.

Generic Interface Implementations Must Be Unique

Note

The names of generic interfaces do not clash with non-generic interfaces. For example, in the preceding code, we could havealso declared anon-generic interface named IMyIfc.

Generic Delegates

Generic delegates are very much like non-generic delegates, except that the type parameters determine the characteristics of what methods will be accepted.

  • To declare a generic delegate, place the type parameter list in angle brackets after the delegate name, and before the delegate parameter list.

    Generic Delegates
  • Notice that there are two parameter lists: the delegate formal parameter list and the type parameter list.

  • The scope of the type parameters includes

    • The return type

    • The formal parameter list

    • The constraint clauses

The following code shows an example of a generic delegate. In Main, generic delegate MyDelegate is instantiated with an argument of type string and initialized with method PrintString.

delegate void MyDelegate<T>(T value);             // Generic delegate

   class Simple
   {
      static public void PrintString(string s)       // Method matches delegate
      {
         Console.WriteLine(s);
      }

      static public void PrintUpperString(string s)  // Method matches delegate
      {
         Console.WriteLine("{0}", s.ToUpper());
      }
   }

   class Program
   {
      static void Main( )
      {
         var myDel =                                 // Create inst of delegate
            new MyDelegate<string>(Simple.PrintString);
         myDel += Simple.PrintUpperString;           // Add a method.

         myDel("Hi There.");                         // Call delegate
      }
   }

This code produces the following output:

Hi There.
HI THERE.

Another Generic Delegate Example

Since the LINQ feature of C# 3.0 uses generic delegates extensively, I think it's worth showing another example before we get there. I'll cover LINQ itself, and more about its generic delegates, in Chapter 21.

The following code declares a generic delegate named Func, which takes methods with two parameters and that return a value. The method return type is represented as TR, and the method parameter types are represented as T0 and T1. Notice that the delegate return type is last in the generic parameter list.

Another Generic Delegate Example

This code produces the following output:

Total: 28

Generic Methods

Unlike the other generics, a method is a member, not a type.

Generic methods can be declared in both generic and non-generic classes, and in structs and interfaces, as shown in Figure 19-10.

Generic methods can be declared in generic and non-generic types.

Figure 19-10. Generic methods can be declared in generic and non-generic types.

Declaring a Generic Method

Generic methods, like the other generics, have a type parameter list and optional constraints.

  • Generic methods, like generic delegates, have two parameter lists:

    • The method parameter list, enclosed in parentheses

    • The type parameter list, enclosed in angle brackets

  • To declare a generic method, do the following:

    • Place the type parameter list immediately after the method name and before the method parameter list.

    • Place the optional constraint clauses after the method parameter list.

Declaring a Generic Method

Note

Remember that the type parameter list goes after the method name and before the method parameter list.

Invoking a Generic Method

To invoke a generic method, supply type arguments with the method invocation, as shown here:

Invoking a Generic Method

Figure 19-11 shows the declaration of a generic method called DoStuff, which takes two type parameters. Below it are two invocations of the method, with different sets of type parameters. Each invocation produces a different version of the method, as shown on the right of the figure.

A generic method with two instantiations

Figure 19-11. A generic method with two instantiations

Inferring Types

If you are passing parameters into a method, the compiler can sometimes infer from the types of the method parameters the types that should be used as the type parameters of the generic method. This can make the method calls simpler and easier to read.

For example, the following code declares MyMethod, which takes a method parameter of the same type as the type parameter.

Inferring Types

If you invoke MyMethod with a variable of type int, as shown in the following code, the information in the type parameter of the method invocation is redundant, since the compiler can see from the method parameter that it is an int.

Inferring Types

Since the compiler can infer the type parameter from the method parameter, you can omit the type parameter and its angle brackets from the invocation, as shown here:

MyMethod(MyInt);

Example of a Generic Method

The following code declares a generic method called ReverseAndPrint in a non-generic class called Simple. The method takes as its parameter an array of any type. Main declares three different array types. It then calls the method twice with each array. The first time it calls the method with a particular array, it explicitly uses the type parameter. The second time, the type is inferred.

class Simple                                      // Non-generic class
   {
      static public void ReverseAndPrint<T>(T[] arr)     // Generic method
      {
         Array.Reverse(arr);
         foreach (T item in arr)                               // Use type argument T.
            Console.Write("{0}, ", item.ToString());
         Console.WriteLine("");
      }
   }

   class Program
   {
      static void Main()
      {
         // Create arrays of various types.
         var intArray    = new int[]    { 3, 5, 7, 9, 11 };
         var stringArray = new string[] { "first", "second", "third" };
         var doubleArray = new double[] { 3.567, 7.891, 2.345 };

         Simple.ReverseAndPrint<int>(intArray);        // Invoke method
         Simple.ReverseAndPrint(intArray);             // Infer type and invoke

         Simple.ReverseAndPrint<string>(stringArray);  // Invoke method
         Simple.ReverseAndPrint(stringArray);          // Infer type and invoke

         Simple.ReverseAndPrint<double>(doubleArray);  // Invoke method
         Simple.ReverseAndPrint(doubleArray);          // Infer type and invoke
      }
   }

This code produces the following output:

11 9 7 5 3
3 5 7 9 11
third second first
first second third
2.345 7.891 3.567
3.567 7.891 2.345

Extension Methods with Generic Classes

Extension methods are described in detail in Chapter 7 and work just as well with generic classes. They allow you to associate a static method in one class with a different generic class, and to invoke the method as if it were an instance method on a constructed instance of the class.

As with non-generic classes, an extension method for a generic class

  • Must be declared static

  • Must be the member of a static class

  • Must contain as its first parameter type the keyword this, followed by the name of the generic class it extends

The following code shows an example of an extension method called Print on a generic class called Holder<T>:

static class ExtendHolder
    {
        public static void Print<T>(this Holder<T> h)
        {
            T[] vals = h.GetValues();
            Console.WriteLine("{0},	{1},	{2}", vals[0], vals[1], vals[2]);
        }
    }

    class Holder<T>
    {
        T[] Vals = new T[3];

        public Holder(T v0, T v1, T v2)
        { Vals[0] = v0; Vals[1] = v1; Vals[2] = v2; }

        public T[] GetValues() { return Vals; }
    }

    class Program
    {
        static void Main(string[] args) {
            var intHolder    = new Holder<int>(3, 5, 7);
            var stringHolder = new Holder<string>("a1", "b2", "c3");
            intHolder.Print();
            stringHolder.Print();
        }
    }

This code produces the following output:

3,      5,      7
a1,     b2,     c3
..................Content has been hidden....................

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