Generics
Intro
Generics let you write type-safe, reusable code without duplicating logic per type. Instead of accepting object and casting later, you keep strong compile-time guarantees and better IDE support. In .NET, generics also matter for performance because collections like List<int> avoid boxing that older non-generic APIs caused.
Tis a placeholder for a type chosen by the caller.List<T>is an open generic type definition;List<int>is a closed constructed type.- Constraints (
where T : ...) are capability contracts that unlock members safely. - Generic code is checked at compile time, then JIT-optimized per runtime type usage.
Use Cases
- Collections:
List<T>,Dictionary<TKey, TValue>, andHashSet<T>provide reusable containers with strong typing. - Reusable algorithms: sorting, filtering, and comparison helpers can work across many types with constraints.
- Repository/service abstractions: patterns like
IRepository<TEntity>avoid repeating CRUD interfaces per entity. - Result wrappers: types like
Result<T>orApiResponse<T>let you model success payloads consistently.
Constraints
Constraints define what operations are legal on T and protect APIs from invalid type arguments.
where T : class-Tmust be a reference type.where T : struct-Tmust be a non-nullable value type.where T : notnull-Tcannot be nullable (string?,int?, etc.).where T : unmanaged-Tmust be blittable/unmanaged (useful for low-level memory scenarios).where T : new()-Tmust have a public parameterless constructor.where T : BaseType-Tmust inherit from a specific base type.where T : ISomeInterface-Tmust implement a specific interface.
Variance
Variance controls assignment compatibility between constructed generic types.
- Invariance (default):
List<string>is not assignable toList<object>. - Covariance (
out T): lets you use a more derived type where a base type is expected for producer-only APIs (for example,IEnumerable<string>toIEnumerable<object>). - Contravariance (
in T): lets you use a less derived type where a more derived type is expected for consumer-only APIs (for example,IComparer<object>asIComparer<string>). - Variance is supported on interfaces and delegates marked with
in/out, and only for reference-type substitutions.
IEnumerable<string> names = new List<string> { "Ada", "Linus" };
IEnumerable<object> objects = names; // covariance
Action<object> printAny = o => Console.WriteLine(o);
Action<string> printString = printAny; // contravariance
List<string> list = new();
// List<object> invalid = list; // does not compile (invariance)
Example
public static T CreateAndValidate<T>()
where T : EntityBase, IValidatable, new()
{
var value = new T();
value.Validate();
return value;
}
Pitfalls
- Unconstrained
Tblocks member/operator usage because the compiler cannot prove capabilities, which pushes unsafe casts and weakens API clarity; add the smallest constraint set (where T : IFoo,where T : struct, etc.) that encodes what the algorithm really needs. default(T)can hide correctness bugs because reference and nullable types becomenullwhile value types become zeroed data, which may be interpreted as valid business values; model absence explicitly (for example,Trypattern,Option, or nullable annotations) and validate before use.- Over-constraining (
where T : class, SomeConcreteType) couples generic APIs to one hierarchy, which prevents reuse and forces duplicate implementations later; prefer interface-based constraints that describe behavior instead of concrete inheritance chains.
Tradeoffs
- Generics vs
object(boxing): Non-generic collections (ArrayList,Hashtable) store value types asobject, boxing them on every add and unboxing on every read.List<int>avoids boxing entirely — the JIT generates a specialized implementation per value type. The performance difference is measurable in allocation-heavy loops on value types likeint,Guid, orDateTime. - Generics vs inheritance for polymorphism: Generics express static (compile-time) polymorphism — the type argument is resolved at JIT time. Inheritance expresses dynamic (runtime) polymorphism via virtual dispatch. Use generics when the concrete type is always known at the call site (algorithm or container); use inheritance when the concrete type is determined at runtime (strategy, plugin, handler).
- CLR generic specialization: The CLR generates separate JIT-compiled bodies for each value-type argument (
List<int>,List<double>each get their own code) but shares one compiled body for all reference-type arguments (List<string>andList<object>share JIT output). This means value-type generics are as fast as hand-typed code, while reference-type generics share an efficient single body with a small type-pointer indirection.
Questions
IEnumerable<string> assign to IEnumerable<object>, but List<string> does not assign to List<object>?
IEnumerable<out T> is covariant, so it is safe to upcast because it only produces T values.
List<T> is invariant because it both reads and writes T; if List<string> were assignable to List<object>, code could add a non-string object and break type safety.
In practice, expose covariant interfaces (IEnumerable<T>, IReadOnlyList<T>) at API boundaries and keep mutable concrete collections internal.
out or in?
Use out when the type parameter is output-only (returned values), and in when it is input-only (method arguments).
If a parameter must be both consumed and produced, keep it invariant because variance would allow unsafe assignments.
This design choice improves API flexibility without sacrificing compile-time safety.
default(T) as a fallback value. Why can this be dangerous in production code?
default(T) can silently map to meaningful domain values (0, DateTime.MinValue, null), so failures look like valid data instead of explicit errors.
Repeated fallback usage can spread bad state across caches, persistence, or downstream services before detection.
Prefer explicit failure paths (TryXxx, exceptions, discriminated result types) and validate invariants at boundaries.
Links
- Generics in C# — official guide covering generic classes, methods, interfaces, and delegates with examples.
- Constraints on type parameters — full list of constraint keywords and their semantics.
- Covariance and contravariance in generics — Microsoft reference on
in/outvariance with interface and delegate examples. - Covariance and Contravariance in C# (Eric Lippert) — 10-part series by a former C# compiler team member; the definitive practitioner explanation of variance semantics.