C H A P T E R  17

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 that implements a stack of ints. It allows you to push ints onto the stack and pop them off. This, by the way, isn’t the system stack.

   class MyIntStack                        // Stack for ints
   {
      int   StackPointer = 0;
      int[] StackArray;                    // Array of int
                        int
       int                   
      public void Push( int x )            // Input type: int
      {
         ...
      }       int
              
      public int Pop()                     // Return type: int
      {
         ...
      }
   
        ...
   }

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:

  1. Cut and paste the code for class MyIntStack.
  2. Change the class name to MyFloatStack.
  3. Change the appropriate int declarations to float declarations throughout the class declaration.
   class MyFloatStack                     // Stack for floats
   {
      int   StackPointer = 0;
      float [] StackArray;                // Array of float
                        float
       float                 
      public void Push( float x )         // Input type: float
      {
         ...
      }
              float
               
      public float Pop()                  // Return type: float
      {
         ...
      }
   
      ...
   
   }

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, and so on).
  • 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#

The generics feature offers a more elegant way 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. This means 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 17-1 illustrates this point.

Image

Figure 17-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 17-2 shows how generic types fit in with the other types covered.

Image

Figure 17-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:

  1. Take the MyIntStack class declaration, and instead of substituting float for int, substitute the type placeholder T.
  2. Change the class name to MyStack.
  3. 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.

   class MyStack <T>
   {
      int StackPointer = 0;
      T [] StackArray;
      
                       
      public void Push(T x ) {...}

             
      public T Pop() {...}
         ...
   }

Generic Classes

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

As you know, there are two steps for creating and using your own regular, nongeneric classes: declaring the class and creating instances of the class. However, 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 17-3 illustrates the process at a high level. If it’s not all completely clear yet, don’t worry—I’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. This is called a constructed type.
  3. Create instances of the constructed type.
Image

Figure 17-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:

  • You place a matching set of angle brackets after the class name.
  • Between the angle brackets, you place a comma-separated list of the placeholder strings that represent the types, to be supplied on demand. These are called type parameters.
  • You 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.

                  Type parameters
                          
   class SomeClass < T1, T2 >
   {   Normally, types would be used in these positions.
                            
      public T1 SomeVar  = new T1();
      public T2 OtherVar = new T2();
   }                                             
        Normally, types would be used in these positions.

There is no special keyword that flags a generic class declaration. Instead, 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

Once you have declared the generic type, 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 constructed type, which is a template from which it creates actual class objects.

The syntax for creating the constructed type is shown below, and consists of listing the class name and supplying 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.

            Type arguments
                       
   SomeClass< short, int >

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 17-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.

Image

Figure 17-4. Supplying type arguments for all the type parameters of a generic class allows the compiler to produce a constructed class from which actual class objects can be created.

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

  • Generic class declarations have type parameters, which act as placeholders for types.
  • Type arguments are the actual types you supply when creating a constructed type.
Image

Figure 17-5. Type parameters vs. 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, nongeneric 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 equal sign, it uses the var keyword to make the compiler use type inference.
   MyNonGenClass         myNGC = new MyNonGenClass        ();
       Constructed class                    Constructed class
                                                         
   SomeClass<short, int>  mySc1 = new SomeClass<short  int>();
   var                    mySc2 = new SomeClass<short, int>();

As with nongeneric classes, the reference and the instance can be created separately, as shown in Figure 17-6. The figure also shows that what is going on in memory is the same as for a nongeneric 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.
Image

Figure 17-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 nongeneric class declaration.

For example, the following code shows the creation of two types from generic class SomeClass. The code is illustrated in Figure 17-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
   
            ...
Image

Figure 17-7. Two different 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( )
      {
         MyStack<int>    StackInt    = new MyStack<int>();
         MyStack<string> StackString = new MyStack<string>();

         StackInt.Push(3);
         StackInt.Push(5);
         StackInt.Push(7);
         StackInt.Push(9);
         StackInt.Print();

         StackString.Push("This is fun");
         StackString.Push("Hi there!  ");
         StackString.Print();
      }
   }

This code produces the following output:


   Value: 9
   Value: 7
   Value: 5
   Value: 3

   Value: Hi there!
   Value: This is fun


Comparing the Generic and Nongeneric Stack

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

Image

Image

Figure 17-8. Nongeneric stack vs. 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 didn’t 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 can’t 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’s 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 doesn’t 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 variables of the same generic type. LessThan attempts to return the result of using the less-than operator. But not all classes implement the less-than operator, so you can’t just substitute any class for T. The compiler, therefore, 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 types that meet the constraints can be substituted for the given type parameter to produce constructed types.

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:

         Type parameter          Constraint list
                                                
   where  TypeParam : constraint, constraint, ...
                   
   Keyword          Colon

The important points about where clauses are the following:

  • They’re listed after the closing angle bracket of the type parameter list.
  • They’re 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.

             Unbounded   With constraints
                                No separators
   class MyClass < T1, T2, T3 >      
                   where T2: Customer                   // Constraint for T2
                   where T3: IComparable                // Constraint for T3
   {                                    
      ...                           No separators
   }

Constraint Types and Order

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

Image

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 17-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.
Image

Figure 17-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 Methods

Unlike the other generics, a method is not a type but a member. You can declare generic methods in both generic and nongeneric classes, and in structs and interfaces, as shown in Figure 17-10.

Image

Figure 17-10. Generic methods can be declared in generic and nongeneric types.

Declaring a Generic Method

Generic methods have a type parameter list and optional constraints.

  • Generic methods 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 any constraint clauses after the method parameter list.
                    Type parameter list           Constraint clauses
                                                         
   public void PrintData<S, T> (S p, T t) where S: Person
   {                                
   ...                       Method parameter list
   }

Image 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:

           Type arguments
                      
   MyMethod<short, int>();
   MyMethod<int, long >();

Figure 17-11 shows the declaration of a generic method called DoStuff, which takes two type parameters. Below it are two places where the method is called, each with a different set of type parameters. The compiler uses each of these constructed instances to produce a different version of the method, as shown on the right of the figure.

Image

Figure 17-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.

   public void MyMethod <T> (T myVal) { ... }
                            
                     Both are of type T

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’s an int.

   int myInt = 5;
   MyMethod <int> (myInt);
                    
             Both are ints

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 nongeneric 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 nongeneric classes, an extension method for a generic class must satisfy the following constraints:

  • It must be declared static.
  • It must be the member of a static class.
  • It 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

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.

   struct PieceOfData<T>                             // Generic struct
   {
      public PieceOfData(T value) { _data = value; }
      private T _data;
      public  T Data
      {
         get { return _data; }
         set { _data = value; }
      }
   }
   
   class Program
   {
      static void Main()          Constructed type
      {                                           
         var intData    = new PieceOfData<int>(10);
         var stringData = new PieceOfData<string>("Hi there.");
                                            
                                      Constructed type
         Console.WriteLine("intData    = {0}", intData.Data);
         Console.WriteLine("stringData = {0}", stringData.Data);
      }
   }

This code produces the following output:


intData    = 10
stringData = Hi there.

Generic Delegates

Generic delegates are very much like nongeneric 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.
                         Type parameters
                                 
       delegate R MyDelegate<T, R>( T value );
                                      
             Return type         Delegate formal parameter
  • 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 following:
    • 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 C#’s LINQ feature uses generic delegates extensively, it’s worth showing another example before we get there. I’ll cover LINQ itself, and more about its generic delegates, in Chapter 19.

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 T1 and T2.

                           Delegate parameter type
                                        
   public delegate TR Func<T1, T2, TR>(T1 p1, T2 p2);  // Generic delegate
                                   
   class Simple     Delegate return type
   {
      static public string PrintString(int p1, int p2) // Method matches delegate
      {
         int total = p1 + p2;
         return total.ToString();
      }
   }

   class Program
   {
      static void Main()
      {
         var myDel =                                   // Create inst of delegate.
            new Func<int, int, string>(Simple.PrintString);

         Console.WriteLine("Total: {0}", myDel(15, 13));  // Call delegate.
      }
   }

This code produces the following output:


Total: 28

Generic Interfaces

Generic interfaces allow you to write interfaces where the formal parameters and return types of interface members are generic type parameters. Generic interface declarations are similar to nongeneric 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.

  • Simple is a generic class that implements generic interface IMyIfc.
  • Main instantiates two objects of the generic class: one with type int and the other with type string.
                Type parameter
                    
   interface IMyIfc<T>                          // Generic interface
   {
      T ReturnIt(T inValue);
   }
        Type parameter   Generic interface
                            
   class Simple<S> : IMyIfc<S>                  // Generic class
   {
      public S ReturnIt(S inValue)              // Implement generic interface.
      { return inValue; }
   }
   
   class Program
   {
      static void Main()
      {
         var trivInt    = new Simple<int>();
         var trivString = new Simple<string>();
   
         Console.WriteLine("{0}", trivInt.ReturnIt(5));
         Console.WriteLine("{0}", trivString.ReturnIt("Hi there."));
      }
   }

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 nongeneric type.

For example, the following code is similar to the last example, but in this case, Simple is a nongeneric 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.

   interface IMyIfc<T>                            // Generic interface
   {
      T ReturnIt(T inValue);
   }
          Two different interfaces from the same generic interface
                                          
   class Simple : IMyIfc<int>, IMyIfc<string>     // Nongeneric class
   {
      public int ReturnIt(int inValue)            // Implement interface using int.
      { return inValue; }
   
      public string ReturnIt(string inValue)      // Implement interface using string.
      { return inValue; }
   }
   
   class Program
   {
      static void Main()
      {
         Simple trivial = new Simple();
   
         Console.WriteLine("{0}", trivial.ReturnIt(5));
         Console.WriteLine("{0}", trivial.ReturnIt("Hi there."));
      }
   }

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.

There’s nothing wrong with the second interface in itself, since it’s perfectly fine to use a generic interface. The problem here, though, is that it allows a possible 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.

   interface IMyIfc<T>
   {
      T ReturnIt(T inValue);
   }
                           Two interfaces
                                         
   class Simple<S> : IMyIfc<int>, IMyIfc<S>     // Error!
   {
      public int ReturnIt(int inValue)   // Implement first interface.
      {
         return inValue;
      }
   
      public S ReturnIt(S inValue)       // Implement second interface,
      {                                  // but if it's int, it would be
         return inValue;                 // the same as the one above.
      }
   }

Image Note  The names of generic interfaces do not clash with nongeneric interfaces. For example, in the preceding code, we could have also declared a nongeneric interface named IMyIfc.

Covariance

As you’ve seen throughout this chapter, when you create an instance of a generic type, the compiler takes the generic type declaration and the type arguments and creates a constructed type. A mistake that people commonly make, however, is to assume that you can assign a delegate of a derived type to a variable of a delegate of a base type. In the following sections, we’ll look at this topic, which is called variance. There are three types of variance—covariance, contravariance, and invariance.

We’ll start by reviewing something you’ve already learned: every variable has a type assigned to it, and you can assign an object of a more derived type to a variable of one of its base types. This is called assignment compatibility. The following code demonstrates assignment compatibility with a base class Animal and a class Dog derived from Animal. In Main, you can see that the code creates an object of type Dog and assigns it to variable a2 of type Animal.

   class Animal
   {
      public int NumberOfLegs = 4;
   }

   class Dog : Animal
   {
   }

   class Program
   {
      static void Main( )
      {
         Animal a1 = new Animal( );
         Animal a2 = new Dog( );

         Console.WriteLine( "Number of dog legs: {0}", a2.NumberOfLegs );
      }
   }

This code produces the following output:


Number of dog legs: 4

Figure 17-12 illustrates assignment compatibility. In this figure, the boxes showing the Dog and Animal objects also show their base classes.

Image

Figure 17-12. Assignment compatibility means that you can assign a reference of a more derived type to a variable of a less derived type.

Now let’s look at a more interesting case by expanding the code in the following ways, as shown in the code below:

  • This code adds a generic delegate named Factory, which takes a single type parameter T, takes no method parameters, and returns an object of type T.
  • I’ve added a method named MakeDog that takes no parameters and returns a Dog object. This method, therefore, matches delegate Factory if we use Dog as the type parameter.
  • The first line of Main creates a delegate object whose type is delegate Factory<Dog> and assigns its reference to variable dogMaker, of the same type.
  • The second line attempts to assign a delegate of type delegate Factory<Dog> to a delegate type variable named animalMaker of type delegate Factory<Animal>.

This second line in Main, however, causes a problem, and the compiler produces an error message saying that it can’t implicitly convert the type on the right to the type on the left.

   class Animal       { public int Legs = 4; }  // Base class
   class Dog : Animal { }                       // Derived class

   delegate T Factory<T>( );       ← delegate Factory

   class Program
   {
      static Dog MakeDog( )        ← Method that matches delegate Factory
      {
         return new Dog( );
      }

      static void Main( )
      {
         Factory<Dog>    dogMaker    = MakeDog;   ← Create delegate object.
         Factory<Animal> animalMaker = dogMaker;  ← Attempt to assign delegate object.

         Console.WriteLine( animalMaker( ).Legs.ToString( ) );
      }
   }

It seems to make sense that a delegate constructed with the base type should be able to hold a delegate constructed with the derived type. So why does the compiler give an error message? Doesn’t the principle of assignment compatibility hold?

The principle does hold, but it doesn’t apply in this situation! The problem is that although Dog derives from Animal, delegate Factory<Dog> does not derive from delegate Factory<Animal>. Instead, both delegate objects are peers, deriving from type delegate, which derives from type object, as shown in Figure 17-13. Neither delegate is derived from the other, so assignment compatibility doesn’t apply.

Image

Figure 17-13. Assignment compatibility doesn’t apply because the two delegates are unrelated by inheritance.

Although the mismatch of delegate types doesn’t allow assigning one type to the variable of another type, it’s too bad in this situation, because in the example code, any time we would execute delegate animalMaker, the calling code would expect to have a reference to an Animal object returned. If it returned a reference to a Dog object instead, that would be perfectly fine since a reference to a Dog is a reference to an Animal, by assignment compatibility.

Looking at the situation more carefully, we can see that for any generic delegate, if a type parameter is used only as an output value, then the same situation applies. In all such situations, you would be able to use a constructed delegate type created with a derived class, and it would work fine, since the invoking code would always be expecting a reference to the base class—which is exactly what it would get.

This constant relation between the use of a derived type only as an output value and the validity of the constructed delegate is called covariance. To let the compiler know that this is what you intend, you must mark the type parameter in the delegate declaration with the out keyword.

If we change the delegate declaration in the example by adding the out keyword, as shown here, the code compiles and works fine:

   delegate T Factory<out T>( );
                                         
            Keyword specifying covariance
              of the type parameter

Figure 17-14 illustrates the components of covariance in this example:

  • The variable on the stack, on the left, is of type delegate T Factory<out T>(), where type variable T is of class Animal.
  • The actual constructed delegate in the heap, on the right, was declared with a type variable of class Dog, which is derived from class Animal.
  • This is acceptable because when the delegate is called, the calling code receives an object of type Dog, instead of the expected object of type Animal. The calling code can freely operate on the Animal part of the object, as it expects to do.
Image

Figure 17-14. The covariant relationship allows a more derived type to be in return and out positions.

Contravariance

Now that you understand covariance, let’s take a look at a related situation. The following code declares a delegate named Action1 that takes a single type parameter and a single method parameter whose type is that of the type parameter, and it returns no value.

The code also contains a method called ActOnAnimal, whose signature and void return type match the delegate declaration.

The first line in Main creates a constructed delegate using type Animal and method ActOnAnimal, whose signature and void return type match the delegate declaration. In the second line, however, the code attempts to assign the reference to this delegate to a stack variable named dog1, of type delegate Action1<Dog>.

   class Animal { public int NumberOfLegs = 4; }
   class Dog : Animal { }

   class Program         Keyword for contravariance
   {                        
      delegate void Action1<in T>( T a );

      static void ActOnAnimal( Animal a ) { Console.WriteLine( a.NumberOfLegs ); }

      static void Main( )
      {
         Action1<Animal> act1 = ActOnAnimal;
         Action1<Dog>    dog1 = act1;
         dog1( new Dog() );
      }
   }

This code produces the following output:


4

Like the previous situation, by default, you can’t assign the two incompatible types. But also like the previous situation, there are scenarios where the assignment would work perfectly fine.

In fact, this is true whenever the type parameter is used only as an input parameter to the method in the delegate. The reason for this is that even though the invoking code passes in a reference to a more derived class, the method in the delegate is only expecting a reference to a less derived class—which of course it receives and knows how to manipulate.

This relation, allowing a more derived object where a less derived object is expected, is called contravariance. To use it, you must use the in keyword with the type parameter, as shown in the code.

Figure 17-15 illustrates the components of contravariance in line 2 of Main.

  • The variable on the stack, on the left, is of type delegate void Action1<in T>(T p), where the type variable is of class Dog.
  • The actual constructed delegate, on the right, is declared with a type variable of class Animal, which is a base class of class Dog.
  • This works fine because when the delegate is called, the calling code passes in an object of type Dog to method ActOnAnimal, which is expecting an object of type Animal. The method can freely operate on the Animal part of the object, as it expects to do.
Image

Figure 17-15. The contravariant relationship allows more derived types to be allowed as input parameters.

Figure 17-16 summarizes the differences between covariance and contravariance in a generic delegate.

  • The top figure illustrates covariance.
    • The variable on the stack on the left is of type delegate F<out T>( ), where the type parameter is of a class named Base.
    • The actual constructed delegate, on the right, was declared with a type parameter of class Derived, which is derived from class Base.
    • This works fine because when the delegate is called, the method returns a reference to an object of the derived type, which is also a reference to the base class, which is exactly what the calling code is expecting.
  • The bottom figure illustrates contravariance.
    • The variable on the stack, on the left, is of type delegate void F<in T>(T p), where the type parameter is of class Derived.
    • The actual constructed delegate, on the right, was declared with a type parameter of class Base, which is a base class of class Derived.
    • This works fine because when the delegate is called, the calling code passes in an object of the derived type to the method, which is expecting an object of the base type. The method can operate freely on the base part of the object, as it expects to do.
Image

Figure 17-16. A comparison of covariance and contravariance

Covariance and Contravariance in Interfaces

You should now have an understanding of covariance and contravariance as it applies to delegates. The same principles apply to interfaces, including the syntax using the out and in keywords in the interface declaration.

The following code shows an example of using covariance with an interface. The things to note about the code are the following:

  • The code declares a generic interface with type parameter T. The out keyword specifies that the type parameter is covariant.
  • Generic class SimpleReturn implements the generic interface.
  • Method DoSomething shows how a method can take an interface as a parameter. This method takes as its parameter a generic IMyIfc interface constructed with type Animal.

The code works in the following way:

  • The first two lines of Main create and initialize a constructed instance of generic class SimpleReturn, using class Dog.
  • The next line assigns that object to a variable on the stack that is declared of constructed interface type IMyIfc<Animal>. Notice several things about this declaration:
    • The type on the left of the assignment is an interface type—not a class.
    • Even though the interface types don’t exactly match, the compiler allows them because of the covariant out specifier in the interface declaration.
  • Finally, the code calls method DoSomething with the constructed covariant class that implements the interface.
    class Animal { public string Name; }
    class Dog: Animal{ };
                  Keyword for covariance
                      
    interface IMyIfc<out T>
    {
        T GetFirst();
    }

    class SimpleReturn<T>: IMyIfc<T>
    {
        public T[] items = new T[2];
        public T GetFirst() { return items[0]; }
    }

    class Program
    {
        static void DoSomething(IMyIfc<Animal> returner)
        {
            Console.WriteLine(returner.GetFirst().Name);
        }

        static void Main( )
        {
            SimpleReturn<Dog> dogReturner = new SimpleReturn<Dog>();
            dogReturner.items[0] = new Dog() { Name = "Avonlea" };

            IMyIfc<Animal> animalReturner = dogReturner;

            DoSomething(dogReturner);
        }
    }

This code produces the following output:


Avonlea

More About Variance

The previous two sections explained explicit covariance and contravariance. There is also a situation where the compiler automatically recognizes that a certain constructed delegate is covariant or contravariant and makes the type coercion automatically. This happens when the object hasn’t yet had a type assigned to it. The following code shows an example.

The first line of Main creates a constructed delegate of type Factory<Animal> from a method where the return type is a Dog object, not an Animal object. When Main creates this delegate, the method name on the right side of the assignment operator isn’t yet a delegate object, and hence doesn’t have a delegate type. At this point the compiler can determine that the method matches the type of the delegate, with the exception t hat its return type is of type Dog rather than type Animal. The compiler is smart enough to realize that this is a covariant relation and creates the constructed type and assigns it to the variable.

Compare that with the assignments in the third and fourth lines of Main. In these cases, the expressions on the right side of the equal sign are already delegates, and hence have a delegate type. These, therefore, need the out specifier in the delegate declaration to signal the compiler to allow them to be covariant.

   class Animal { public int Legs = 4; }               // Base class
   class Dog : Animal { }                              // Derived class

   class Program
   {
      delegate T Factory<out T>();

      static Dog MakeDog() { return new Dog(); }

      static void Main()
      {
          Factory<Animal> animalMaker1 = MakeDog;      // Coerced implicitly

          Factory<Dog>    dogMaker     = MakeDog;
          Factory<Animal> animalMaker2 = dogMaker;     // Requires the out specifier

          Factory<Animal> animalMaker3
                    = new Factory<Dog>(MakeDog);       // Requires the out specifier
      }
   }

Here are some other important things you should know about variance:

  • As you’ve seen, variance deals with the issue of where it’s safe to substitute a base type for a derived type, and vice versa. Variance, therefore, applies only to reference types—since you can’t derive other types from value types.
  • Explicit variance, using the in and out keywords, applies only to delegates and interfaces—not classes, structs, or methods.
  • Delegate and interface type parameters that don’t include either the in or out keyword are called invariant. These types cannot be used covariantly or contravariantly.
                         Contravariant
                                
  delegate T Factory<out R, in S, T>( );
                                 
                 Covariant        Invariant
..................Content has been hidden....................

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