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.
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 = newT
[100]; public void Push (T
obj) { data[position++] = obj; } publicT
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
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; } publicint
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.
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.
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> {}
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
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); }
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:
whereT
:base-class
// Base-class constraint whereT
:interface
// Interface constraint whereT
: class // Reference-type constraint whereT
: struct // Value-type constraint whereT
: new() // Parameterless constructor // constraint whereU
: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.
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> {...}
A type can name itself as the concrete type when closing a type argument:
public interface IEquatable<T> { bool Equals (T obj); } public classBalloon
: 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 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
Assuming A
is convertible to
B
, X
is covariant if X<A>
is convertible to X<B>
.
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.
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.
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).