Generics let you write a type or method once and use it with many types, without losing type safety. Instead of a List of object that needs casts and accepts anything, you get List<int> and List<Order>, each checked by the compiler. The placeholder in angle brackets, conventionally T, is a type parameter; the type a caller supplies is the type argument.
Why generics: the problem they solve
Before generics (C# 1), collections stored object. That compiled, but mistakes surfaced only at run time, and every int was boxed on the way in:
var prices = new System.Collections.ArrayList();
prices.Add(9.99m);
prices.Add("12.50"); // compiles: anything is an object
decimal total = 0;
foreach (object p in prices)
total += (decimal)p; // InvalidCastException on the string
With List<decimal>, the second Add is a compile error, the loop needs no cast, and the decimals are stored as decimals.
Generic classes
Declare type parameters after the class name, then use them like any other type inside the class:
Output:
Page 2/3: Chen, Dara, more: True
Page 1/1: 90, 72, 85, more: False
Page<string> and Page<int> are two different types produced from one definition. A class can take several type parameters (Dictionary<TKey, TValue>); name them T when there is one and TSomething when there are several.
Generic methods and type inference
A method can have its own type parameters, whether or not its class is generic. Callers usually leave out the type argument, because the compiler infers it from the arguments:
Output:
2 1
back front
ho ho ho
4.50 EUR, 12.00 EUR, 0.99 EUR
Inference works from arguments only, never from the return type. Repeat(0, 3) needs no annotation, but a method like T Create<T>() has nothing to infer from, so callers must write Create<Customer>(). Most of LINQ is generic methods relying on this inference: prices.Where(p => p > 1) is Where<decimal> without you writing it.
Constraints: what T is allowed to be
Inside a generic method, the compiler only lets you do what works for every possible T. With no constraint, that is little more than what object supports. To call CompareTo, create an instance, or read a property, tell the compiler what T must be, with where:
Output:
17
plum
19.99
42 Zoe
The constraints you will use:
| Constraint | Meaning | Lets you |
|---|---|---|
where T : class | reference type | assign null, use as |
where T : struct | non-nullable value type | use T? as Nullable<T> |
where T : new() | public parameterless constructor | call new T() (must be listed last) |
where T : Entity | derives from Entity | use Entity's members |
where T : IComparable<T> | implements the interface | call CompareTo |
where T : U | derives from another type parameter | relate two parameters |
where T : unmanaged (C# 7.3) | struct with no references inside | pointers, stackalloc |
where T : Enum (C# 7.3) | an enum type | pass to Enum methods |
where T : notnull (C# 8) | non-nullable type | nullable reference annotations |
Several constraints on one parameter are separated by commas; several parameters each get their own where clause: class Cache<TKey, TValue> where TKey : notnull where TValue : class.
A missing constraint shows up as a compile error at the point you use the member, for example CS1061 ('T' does not contain a definition for 'CompareTo') or CS0304 when calling new T() without new().
default(T)
Generic code sometimes needs "the empty value" of a type it does not know. default(T) gives 0 for numbers, false for bool, null for reference types and nullable types, and a zeroed struct otherwise:
Output:
31
0
Porto
True
0001-01-01
The second line shows the weakness of this pattern: for value types, "not found" and "found a 0" look the same. That is why framework methods use the TryGet shape instead (bool TryGetValue(TKey key, out TValue value)). Since C# 7.1 you can write the shorter return default; when the type is clear from context.
Static members are per constructed type
Each constructed type (Counter<int>, Counter<string>) gets its own copy of the class's static fields:
Output:
2
1
False
This is occasionally useful (a per-type cache such as TypeInfo<T>.Name computed once per T) and occasionally a surprise when you expected one shared counter. Put shared state in a non-generic class.
Generic collections
The generic collections cover most needs: List<T>, Dictionary<TKey, TValue>, HashSet<T>, Queue<T>, Stack<T>, and the interfaces they share, IEnumerable<T>, ICollection<T>, IList<T>, IReadOnlyList<T>. Accept the narrowest interface a method needs (IEnumerable<T> if it only loops), and return a concrete or read-only type.
Covariance and contravariance
A Dog is an Animal, but a List<Dog> is not a List<Animal>:
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs;
// error CS0029: Cannot implicitly convert type 'List<Dog>' to 'List<Animal>'
If that were allowed, animals.Add(new Cat()) would put a cat into the dog list. Interfaces that only produce values are safe, and they are declared covariant with out:
IEnumerable<Animal> readOnly = dogs; // fine: IEnumerable<out T>
IReadOnlyList<Animal> alsoFine = dogs; // fine: IReadOnlyList<out T>
The mirror image, contravariance, marked in, applies to interfaces and delegates that only consume values: an Action<Animal> can be used where an Action<Dog> is expected, because anything that can handle any animal can handle a dog. IComparer<in T> is contravariant, and Func<in T, out TResult> uses both markers. Variance applies only to interfaces and delegates, and only to reference type arguments: IEnumerable<int> does not convert to IEnumerable<object>.
Common mistakes
- Using
objectwhere a type parameter belongs. It gives up compile-time checks and boxes value types. - Comparing two
Tvalues with==. Without aclassconstraint it does not compile (CS0019), and with one it compares references. UseEqualityComparer<T>.Default.Equals(a, b), which works for everyTand respects the type's ownEquals. - Returning
default(T)as "not found". It is ambiguous for value types. Usebool TryX(..., out T value). - Too many constraints. Each one narrows who can use the method. Constrain only what the body actually needs.
- Expecting
List<Derived>to convert toList<Base>. AcceptIEnumerable<Base>orIReadOnlyList<Base>instead.
Frequently Asked Questions
What are generics in C#?
Generics let you write a class, interface or method once with a placeholder type, T, that callers fill in: List<int>, List<Customer>. The compiler checks every use against the real type, so you get type safety without casts, and value types are stored without boxing. The collections in System.Collections.Generic are the most familiar example.
How do I write a generic method in C#?
Put the type parameters after the method name: static T Largest<T>(List<T> items) where T : IComparable<T> { ... }. Callers usually omit the type argument because the compiler infers it from the arguments: Largest(prices) works when prices is a List<decimal>. Write it explicitly, Largest<decimal>(...), when there is nothing to infer from.
What does where T : class mean in C#?
It is a constraint: T must be a reference type, so the generic code may assign null to a T and use as T. Other constraints are where T : struct (a non-nullable value type), where T : new() (has a public parameterless constructor, so the code can call new T()), where T : SomeBaseClass, and where T : ISomeInterface, which lets the code call that interface's members on T.
What is default(T) in C#?
The default value of whatever type T turns out to be: 0 for numbers, false for bool, null for reference types and nullable types, and a zeroed struct for other value types. Generic code uses it when it needs "no value" without knowing the type, for example as the return value of a failed lookup. C# 7.1 added the shorter default literal.
Why can't I assign a List<Dog> to a List<Animal> in C#?
Because a List<Animal> accepts any animal: if the assignment were allowed, code could add a Cat to what is really a List<Dog>. The compiler rejects it (CS0029). Read-only interfaces are covariant, so IEnumerable<Animal> animals = dogs; and IReadOnlyList<Animal> animals = dogs; both compile, since nothing can be added through them.