Generics

C# has two separate mechanisms for writing code that is reusable across different types: inheritance and generics. Whereas inheritance expresses reusability with a base type, generics express reusability with a “template” that contains “placeholder” types. Generics, when compared to inheritance, can increase type safety and reduce casting and boxing.

Generic Types

A generic type declares type parameters—placeholder types to be filled in by the consumer of the generic type, which supplies the type arguments. Here is a generic type, Stack<T>, designed to stack instances of type T. Stack<T> declares a single type parameter T:

public class Stack<T>
{
  int position;
  T[] data = new T[100];
  public void Push (T obj) { data[position++] = obj;  }
  public T Pop()           { return data[--position]; }
}

We can use Stack<T> as follows:

Stack<int> stack = new Stack<int>();
stack.Push(5);
stack.Push(10);
int x = stack.Pop();        // x is 10
int y = stack.Pop();        // y is 5

Note

Notice that no downcasts are required in the last two lines, avoiding the possibility of a runtime error and eliminating the overhead of boxing/unboxing. This makes our generic stack superior to a nongeneric stack that uses object in place of T (see The object Type for an example).

Stack<int> fills in the type parameter T with the type argument int, implicitly creating a type on the fly (the synthesis occurs at runtime). Stack<int> effectively has the following definition (substitutions appear in bold, with the class name hashed out to avoid confusion):

public class ###
{
  int position;
  int[] data;
  public void Push (int obj) { data[position++] = obj;  }
  public int Pop()           { return data[--position]; }
}

Technically, we say that Stack<T> is an open type, whereas Stack<int> is a closed type. At runtime, all generic type instances are closed—with the placeholder types filled in.

Generic Methods

A generic method declares type parameters within the signature of a method. With generic methods, many fundamental algorithms can be implemented in a general-purpose way only. Here is a generic method that swaps the contents of two variables of any type T:

static void Swap<T> (ref T a, ref T b)
{
  T temp = a; a = b; b = temp;
}

Swap<T> can be used as follows:

int x = 5, y = 10;
Swap (ref x, ref y);

Generally, there is no need to supply type arguments to a generic method, because the compiler can implicitly infer the type. If there is ambiguity, generic methods can be called with the type arguments as follows:

Swap<int> (ref x, ref y);

Within a generic type, a method is not classed as generic unless it introduces type parameters (with the angle bracket syntax). The Pop method in our generic stack merely consumes the type’s existing type parameter, T, and is not classed as a generic method.

Methods and types are the only constructs that can introduce type parameters. Properties, indexers, events, fields, constructors, operators, and so on cannot declare type parameters, although they can partake in any type parameters already declared by their enclosing type. In our generic stack example, for instance, we could write an indexer that returns a generic item:

public T this [int index] { get { return data[index]; } }

Similarly, constructors can partake in existing type parameters, but cannot introduce them.

Declaring Type Parameters

Type parameters can be introduced in the declaration of classes, structs, interfaces, delegates (see the section Delegates), and methods. A generic type or method can have multiple parameters:

class Dictionary<TKey, TValue> {...}

To instantiate:

var myDic = new Dictionary<int,string>();

Generic type names and method names can be overloaded as long as the number of type parameters differs. For example, the following two type names do not conflict:

class A<T> {}
class A<T1,T2> {}

Note

By convention, generic types and methods with a single type parameter name their parameter T, as long as the intent of the parameter is clear. With multiple type parameters, each parameter has a more descriptive name (prefixed by T).

typeof and Unbound Generic Types

Open generic types do not exist at runtime: open generic types are closed as part of compilation. However, it is possible for an unbound generic type to exist at runtime—purely as a Type object. The only way to specify an unbound generic type in C# is with the typeof operator:

class A<T> {}
class A<T1,T2> {}
...

Type a1 = typeof (A<>);   // Unbound type
Type a2 = typeof (A<,>);  // Indicates 2 type args
Console.Write (a2.GetGenericArguments().Count());  // 2

You can also use the typeof operator to specify a closed type:

Type a3 = typeof (A<int,int>);

or an open type (which is closed at runtime):

class B<T> { void X() { Type t = typeof (T); } }

The default Generic Value

The default keyword can be used to get the default value given for a generic type parameter. The default value for a reference type is null, and the default value for a value type is the result of bitwise-zeroing the type’s fields:

static void Zap<T> (T[] array)
{
  for (int i = 0; i < array.Length; i++)
    array[i] = default(T);
}

Generic Constraints

By default, a type parameter can be substituted with any type whatsoever. Constraints can be applied to a type parameter to require more specific type arguments. There are six kinds of constraint:

where T : base-class   // Base-class constraint
where T : interface    // Interface constraint
where T : class        // Reference-type constraint
where T : struct       // Value-type constraint
where T : new()        // Parameterless constructor
                       // constraint
where U : T            // Naked type constraint

In the following example, GenericClass<T,U> requires T to derive from (or be identical to) SomeClass and implement Interface1, and requires U to provide a parameterless constructor:

class     SomeClass {}
interface Interface1 {}

class GenericClass<T,U> where T : SomeClass, Interface1
                        where U : new()
{ ... }

Constraints can be applied wherever type parameters are defined, whether in methods or in type definitions.

A base-class constraint specifies that the type parameter must subclass (or match) a particular class; an interface constraint specifies that the type parameter must implement that interface. These constraints allow instances of the type parameter to be implicitly converted to that class or interface.

The class constraint and struct constraint specify that T must be a reference type or a (non-nullable) value type, respectively. The parameterless constructor constraint requires T to have a public parameterless constructor and allows you to call new() on T:

static void Initialize<T> (T[] array) where T : new()
{
  for (int i = 0; i < array.Length; i++)
    array[i] = new T();
}

The naked type constraint requires one type parameter to derive from (or match) another type parameter.

Subclassing Generic Types

A generic class can be subclassed just like a nongeneric class. The subclass can leave the base class’s type parameters open, as in the following example:

class Stack<T>                   {...}
class SpecialStack<T> : Stack<T> {...}

Or the subclass can close the generic type parameters with a concrete type:

class IntStack : Stack<int>  {...}

A subtype can also introduce fresh type arguments:

class List<T>                     {...}
class KeyedList<T,TKey> : List<T> {...}

Self-Referencing Generic Declarations

A type can name itself as the concrete type when closing a type argument:

public interface IEquatable<T> { bool Equals (T obj); }

public class Balloon : IEquatable<Balloon>
{
  public bool Equals (Balloon b) { ... }
}

The following are also legal:

class Foo<T> where T : IComparable<T> { ... }
class Bar<T> where T : Bar<T> { ... }

Static Data

Static data is unique for each closed type:

class Bob<T> { public static int Count; }
...
Console.WriteLine (++Bob<int>.Count);     // 1
Console.WriteLine (++Bob<int>.Count);     // 2
Console.WriteLine (++Bob<string>.Count);  // 1
Console.WriteLine (++Bob<object>.Count);  // 1

Covariance

Assuming A is convertible to B, X is covariant if X<A> is convertible to X<B>.

Note

Covariance and contravariance are advanced concepts. The motivation behind their introduction into C# was to allow generic interfaces and generics (in particular, those defined in the Framework, such as IEnumerable<T>) to work more as you’d expect. You can benefit from this without understanding the details behind covariance and contravariance.

(With C#’s notion of variance, “convertible” means convertible via an implicit reference conversion—such as A subclassing B, or A implementing B. Numeric conversions, boxing conversions, and custom conversions are not included.)

For instance, type IFoo<T> is covariant for T if the following is legal:

IFoo<string> s = ...;
IFoo<object> b = s;

As of C# 4.0, generic interfaces permit covariance for type parameters marked with the out modifier (as do generic delegates). To illustrate, suppose that the Stack<T> class that we wrote at the start of this section implements the following interface:

public interface IPoppable<out T> { T Pop(); }

The out modifier on T indicates that T is used only in output positions (e.g., return types for methods). The out modifier flags the interface as covariant and allows us to do this:

// Assuming that Bear subclasses Animal:
var bears = new Stack<Bear>();
bears.Push (new Bear());

// Because bears implements IPoppable<Bear>,
// we can convert it to IPoppable<Animal>:
IPoppable<Animal> animals = bears;   // Legal
Animal a = animals.Pop();

The cast from bears to animals is permitted by the compiler—by virtue of the interface being covariant.

Note

The IEnumerator<T> and IEnumerable<T> interfaces (see Enumeration and Iterators) are marked as covariant from Framework 4.0. This allows you to cast IEnumerable<string> to IEnumerable<object>, for instance.

The compiler will generate an error if you use a covariant type parameter in an input position (e.g., a parameter to a method or a writable property). The purpose of this limitation is to guarantee compile-time type safety. For instance, it prevents us from adding a Push(T) method to that interface which consumers could abuse with the seemingly benign operation of pushing a Camel onto an IPoppable<Animal> (remember that the underlying type in our example is a stack of bears). In order to define a Push(T) method, T must in fact be contravariant.

Note

C# supports covariance (and contravariance) only for elements with reference conversions—not boxing conversions. So, if you wrote a method that accepted a parameter of type IPoppable<object>, you could call it with IPoppable<string>, but not IPoppable<int>.

Contravariance

We previously saw that, assuming that A allows an implicit reference conversion to B, a type X is covariant if X<A> allows a reference conversion to X<B>. A type is contravariant when you can convert in the reverse direction—from X<B> to X<A>. This is supported on interfaces and delegates when the type parameter only appears in input positions, designated with the in modifier. Extending our previous example, if the Stack<T> class implements the following interface:

public interface IPushable<in T> { void Push (T obj); }

we can legally do this:

IPushable<Animal> animals = new Stack<Animal>();
IPushable<Bear> bears = animals;    // Legal
bears.Push (new Bear());

Mirroring covariance, the compiler will report an error if you try to use a contravariant type parameter in an output position (e.g., as a return value, or in a readable property).

..................Content has been hidden....................

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