Generics let you write one class or method that works with any type while keeping full compile-time checking. In C# they are everywhere — lists, dictionaries, tasks — so even if you never write one, you read them constantly. This guide covers writing your own, constraining them, and the variance keywords that confuse people.
The problem#
// Before generics
var list = new ArrayList();
list.Add("hello");
list.Add(42); // no complaint
string s = (string)list[1]; // compiles, throws at run time
Everything went in as object, so nothing was checked and every read needed a cast. Value types were also boxed onto the heap, which cost allocations.
var list = new List<string>();
list.Add("hello");
list.Add(42); // compile error - caught immediately
string s = list[0]; // no cast needed
A generic class#
public class Box<T>
{
private T _value;
public Box(T value) => _value = value;
public T Get() => _value;
public void Set(T value) => _value = value;
public override string ToString() => $"Box({_value})";
}
var text = new Box<string>("hello");
var number = new Box<int>(42);
More than one type parameter is fine:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; }
public TSecond Second { get; }
public Pair(TFirst first, TSecond second)
{
First = first;
Second = second;
}
public Pair<TSecond, TFirst> Swap() => new(Second, First);
}
The conventional names are T for a single type, and descriptive ones such as TKey, TValue or TResult when there are several.
Generic methods#
public static T[] Repeat<T>(T value, int count)
{
var result = new T[count];
for (int i = 0; i < count; i++) result[i] = value;
return result;
}
var zeros = Repeat(0, 5); // int[] - inferred
var blanks = Repeat("", 3); // string[]
var explicitly = Repeat<double>(1, 3);
The type is inferred from the arguments, so you rarely write it out. You only need the explicit form when inference cannot decide — usually when the type appears only in the return value.
Constraints#
Without a constraint, T could be anything, so you can only use members that every type has:
public static T Largest<T>(IEnumerable<T> items) where T : IComparable<T>
{
T best = items.First();
foreach (T item in items)
if (item.CompareTo(best) > 0) best = item;
return best;
}
The full set:
| Constraint | Means |
|---|---|
where T : class |
a reference type |
where T : struct |
a value type, not nullable |
where T : new() |
has a parameterless constructor |
where T : SomeBase |
inherits from that class |
where T : ISomething |
implements that interface |
where T : notnull |
cannot be null |
where T : U |
derives from another type parameter |
public class Factory<T> where T : class, IEntity, new()
{
public T Create() => new T(); // legal because of new()
}
Constraints are a promise to the compiler and a requirement on the caller. Add the minimum that lets your code compile — each one narrows who can use the type.
default(T)#
public static T FirstOrDefault<T>(IEnumerable<T> items)
{
foreach (T item in items) return item;
return default; // null for classes, 0 for int, false for bool
}
Variance#
This is the part people find confusing, and it has a concrete reason behind it:
List<string> strings = new();
List<object> objects = strings; // compile error
If that were allowed, you could then add an int through the second reference, and the list of strings would contain a non-string. The compiler prevents it.
But reading is safe. IEnumerable<T> only produces values, never accepts them, so this works:
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // fine
That is because it is declared IEnumerable<out T>. The keywords say which direction values flow:
out— the type only comes out, as return values. Safe to use a more general type.in— the type only goes in, as parameters. Safe to use a more specific type.
public interface IReader<out T>
{
T Read(); // T only appears as a return type
}
public interface IWriter<in T>
{
void Write(T value); // T only appears as a parameter
}
A list does both, so it can be neither — which is exactly why the first example does not compile.
Generic collections#
List<T> // ordered, grows, index access
Dictionary<TKey, TValue> // key lookup
HashSet<T> // unique values, fast membership tests
Queue<T> // first in, first out
Stack<T> // last in, first out
IEnumerable<T> // anything you can iterate
IReadOnlyList<T> // read-only view
A useful habit: accept the most general type you can and return the most specific that is useful. A method taking IEnumerable<T> works with arrays, lists and LINQ results alike.
Static members are per constructed type#
public class Counter<T>
{
public static int Count;
}
Counter<string>.Count = 5;
Counter<int>.Count = 10;
Console.WriteLine(Counter<string>.Count); // 5 - separate storage
Each closed type gets its own static fields. Occasionally useful, occasionally surprising.
C# does not erase#
var list = new List<string>();
Console.WriteLine(list.GetType().Name); // List`1
if (obj is List<string> strings) { } // works - type is known at run time
Java removes generic type information after compiling; C# keeps it. That means reflection sees the real types, new T() is possible with a constraint, and value types are not boxed — a genuine performance difference.
Questions people ask#
Do generics slow things down?
The opposite, usually. The runtime generates specialised code for value types, which avoids the boxing that ArrayList required.
Can I constrain to a numeric type?
Yes, since C# 11: where T : INumber<T> lets you write arithmetic that works for any numeric type. Before that it needed awkward workarounds.
What is the difference between T and object?
object loses type information — you need casts and can put anything in. T preserves it, so the compiler checks every use and no casting is needed.
When should I write my own generic type?
When you have written the same class twice with only the type changed. Write the concrete version first; generalise when the second use appears.
Where to go next#
- Generics in programming — the same ideas across languages.
- C# collections — the generic types you use daily.
- C# advanced language features — LINQ, which is built on these.