Menu

C# Generics: Generic Classes, Methods and Constraints

How generics work in C#: writing generic classes and methods with type parameters, type inference, constraints with where (class, struct, new(), base classes, interfaces), default(T), static members per type, and covariance with IEnumerable<out T>.

This page includes runnable editors - edit, run, and see output instantly.

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:

ConstraintMeaningLets you
where T : classreference typeassign null, use as
where T : structnon-nullable value typeuse T? as Nullable<T>
where T : new()public parameterless constructorcall new T() (must be listed last)
where T : Entityderives from Entityuse Entity's members
where T : IComparable<T>implements the interfacecall CompareTo
where T : Uderives from another type parameterrelate two parameters
where T : unmanaged (C# 7.3)struct with no references insidepointers, stackalloc
where T : Enum (C# 7.3)an enum typepass to Enum methods
where T : notnull (C# 8)non-nullable typenullable 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 object where a type parameter belongs. It gives up compile-time checks and boxes value types.
  • Comparing two T values with ==. Without a class constraint it does not compile (CS0019), and with one it compares references. Use EqualityComparer<T>.Default.Equals(a, b), which works for every T and respects the type's own Equals.
  • Returning default(T) as "not found". It is ambiguous for value types. Use bool 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 to List<Base>. Accept IEnumerable<Base> or IReadOnlyList<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.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED