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 generic
parameters—placeholder types to be filled in by the consumer of the generic
type, who will supply the generic arguments. Here is a generic type
Stack<T>
, designed to stack instances of type
T. Stack<T>
declares a single generic 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
Stack<int>
fills in the generic parameter
T
with the generic 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. You can only
instantiate a closed type, because all the placeholder types must be filled in.
Generics exist to write code that is reusable across different types. Suppose we
needed a stack of integers, but we didn’t have generic types. One solution would be to
hardcode a separate version of the class for every required element type (e.g., IntStack, StringStack
, etc.) Clearly, this would cause
considerable code duplication. Another solution would be to write a stack that is
generalized by using object
as the element
type:
public class ObjectStack { int position;object
[] data = newobject
[10]; public void Push (object
obj){data[position++] = obj;} publicobject
Pop() { return data[--position];} }
An ObjectStack
, however, wouldn’t work as well as a
hardcoded IntStack
for specifically stacking integers.
Specifically, an ObjectStack
would require boxing and
downcasting that could not be checked at compile time:
// Suppose we just want to store integers here: ObjectStack stack = new ObjectStack(); stack.Push ("s"); // Wrong type, but no error! int i = (int)stack.Pop(); // Downcast - runtime error
What we need is both a general implementation of a stack that works for all element
types, and a way to easily specialize that stack to a specific element type for increased
type safety and reduced casting and boxing. Generics give us precisely this, by allowing
us to parameterize the element type. Stack<T>
has
the benefits of both ObjectStack
and IntStack
. Like ObjectStack,
Stack<T>
is written once to work generally across
all types. Like IntStack, Stack<T>
is
specialized for a particular type—the beauty is that this type is
T
, which we substitute on the fly.
A generic method declares generic parameters within the signature of a method.
With generic methods, many fundamental algorithms can be implemented in only a general-purpose way. Here is a generic method that swaps two values of any type:
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 parameters to a generic method, because the compiler can implicitly infer the type. If there is ambiguity, generic methods can be called with the type parameters as follows:
Swap<int>
(ref x, ref y);
Within a generic type, a method is not classed as generic unless
it introduces
generic parameters (with the angle bracket syntax). The
Pop
method in our generic stack merely uses the
type’s existing generic parameter, T
, and is not
classed as a generic method.
Methods and types are the only constructs that can introduce generic parameters. Properties, indexers, events, fields, methods, operators, and so on cannot declare generic parameters, although they can partake in any generic 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];} }
Generic parameters can be introduced in the declaration of classes, structs,
interfaces, delegates (see the upcoming “Delegates” section), and methods. Other
constructs, such as properties, cannot introduce a generic parameter,
but can use a generic parameter. For example, the property Value
uses T:
public struct Nullable<T>
{ publicT
Value {get;} }
A generic type or method can have multiple parameters. For example:
class Dictionary<TKeyType, TValueType> {...}
To instantiate:
Dictionary<int,string> myDic = new Dictionary<int,string>();
or (in C# 3.0):
var myDic = new Dictionary<int,string>();
Generic type names and method names can be overloaded as long as the number of generic parameters is different. For example, the following two type names do not conflict:
class A<T> {} class A<T1,T2> {}
The typeof
operator requires specifying the number
of parameters when asking for the type of an open type, as follows:
class A<T> {} class A<T1,T2> {} ... Type a1 = typeof(A<>); Type a2 = typeof(A<,>);
Here is an example of asking for the type of a closed type:
Type a3 = typeof(A<int,int>);
The default
keyword can be used to get the default
value given a generic type argument. The default value for a reference type is null
, and the default value for a value type is the result of
bitwise-zeroing the value type’s fields:
static void Zap<T> (T[] array) { for (int i = 0; i < array.Length; i++) array[i] = default(T); }
By default, a generic parameter can be substituted with any type whatsoever. Constraints can be applied to a generic parameter to require more specific type arguments. These are the possible constraints:
whereT : base-class
// Base class constraint whereT : interface
// Interface constraint whereT
: class // Class constraint whereT
: struct // Struct constraint whereT
: new() // Parameterless constructor // constraint whereU : T
// Naked type constraint
In the following example, GenericClass<T>
requires T
to derive from SomeClass
and implement Interface1:
class SomeClass {} interface Interface1 {} class GenericClass<T> where T : SomeClass, Interface1 {}
Constraints can be applied wherever generic parameters are defined, in both methods and type definitions.
A base class constaint or interface
constraint specifies that the type parameter must subclass or implement a
particular class or interface. This allows instances of that type to be implicitly cast to
that class or interface. For example, suppose we want to write a generic Max
method that returns the maximum of two values. We can take
advantage of the generic interface defined in the System
namespace IComparable<T>:
public interface IComparable<T> { int CompareTo (T other); }
CompareTo
returns a positive number if other
is greater than this
.
Using this interface as a constraint, we can write a Max
method as follows (to avoid distraction, null checking is
omitted):
static T Max <T> (T a, T b) where T : IComparable<T> { return a.CompareTo (b) > 0 ? a : b; }
The Max
method can accept arguments of any type
implementing IComparable<T>
(which includes most
built-in types such as int
and string
):
int z = Max (5, 10); // 10 string last = Max ("ant", "zoo"); // zoo
The class constraint and struct constraint
simply specify that T
must be a class
or a struct
. A great
example of the struct constraint is the System.Nullable<T>
struct (we will discuss this class in depth, later,
in the “Nullable Types” section):
struct Nullable<T> where T : struct {...}
The parameterless constructor constraint requires T
to have a public parameterless constructor. If this
constraint is defined, you can 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 generic parameter to derive from another
generic parameter. In this example, the method FilteredStack
returns another Stack
,
containing only the subset of elements where the generic parameter T
is of the generic parameter U:
class Stack<T> { Stack<U> FilteredStack<U>() where U : T {...} }
Generic types are not covariant. This means that even if B
can be cast to A, T<B>
cannot be
cast to T<A>
. For example, suppose Animal
and Bear
are defined
as follows:
class Animal {} class Bear : Animal {}
The following is illegal:
Stack<Bear> bears = new Stack <Bear>(); // compile-time error Stack<Animal> animals = bears;
Lack of covariance can hinder reusability. Suppose, for instance, we
wanted to write a method to Wash
a stack of
animals:
public class ZooCleaner { public static void Wash (Stack<Animal> animals) {...} }
Calling Wash
with a stack of bears would generate a
compile-time error. The workaround is to redefine the Wash
method with a constraint:
public class ZooCleaner
{
public static void Wash<T> (Stack<T> animals)
where T : Animal
{}
}
We can now call Wash
as follows:
Stack<Bear> bears = new Stack<Bear>(); ZooCleaner.Wash (bears);
A generic class can be subclassed just like a nongeneric class. The subclass can leave the base class’s generic 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 subclass can also introduce fresh generic arguments:
class Single<T> { ... }
class Double<T,U
> : Single<T> { ... }
A type can name itself as the concrete type when closing a generic argument:
public interface IEquatable<T> { bool Equals (T obj); } public classBalloon
: IEquatable<Balloon
> { string color; int cc; public bool Equals (Balloon b) { if (b == null) return false; return b.color == color && b.cc == cc; } }
Static data is unique for each closed type:
class Bob<T> { public static int Count; } class Test { static void Main() { Console.WriteLine (++Bob<int>.Count); // 1 Console.WriteLine (++Bob<int>.Count); // 2 Console.WriteLine (++Bob<string>.Count); // 1 Console.WriteLine (++Bob<object>.Count); // 1 } } }
You can instantiate and populate a generic collection in a single step, as follows:
using System.Collections.Generic;
...
List<int> list = new List<int> {1, 2, 3};
The compiler translates this to:
using System.Collections.Generic; ... List<int> list = new List<int>(); list.Add (1); list.Add (2); list.Add (3);
This requires that the collection implements the ICollection<T>
interface, defined in System.Collections.Generic
—the standard .NET interface for mutable
collections.