C# Generic Type Parameter: Syntax, Constraints, and Runtime Behavior
Learn how C# generic type parameters work: syntax, constraints, variance, runtime behavior, and common mistakes for writing type-safe reusable code.
A generic type parameter is a placeholder that lets you write a class, method, or delegate that works with any type while preserving type safety. In C#, you declare the placeholder inside angle brackets after the type or method name, and callers supply a concrete type when they use the member. This mechanism underpins many .NET collections and utility methods, and it is essential for writing reusable, maintainable code.
Declaring a Generic Type Parameter
Introduce a generic type parameter with angle brackets after the type or method name. For a class, write class Repository<T> where T is the type parameter. For a method, the parameter appears after the method name, as in T GetDefault<T>(). Inside the class or method, T can be used like a concrete type for fields, parameters, return types, and local variables, but the actual type is supplied by the caller.
public class Repository<T> { private readonly List<T> _items = new List<T>(); public void Add(T item) { _items.Add(item); } public T Get(int index) { return _items[index]; } }
When you instantiate Repository<int>, T is replaced with int at the call site, giving you a strongly typed collection without casting or boxing. The compiler generates a distinct closed type for each unique type argument, so Repository<int> and Repository<string> are separate types with their own static fields.
Using Multiple Type Parameters
A generic type can declare more than one type parameter. A common built-in example is Dictionary<TKey, TValue>, which needs two independent types. In your own definitions, list the parameters separated by commas.
public class Pair<TFirst, TSecond> { public TFirst First { get; set; } public TSecond Second { get; set; } }
Multiple type parameters are useful when the relationship between types matters, such as a mapper that converts an input to an output. The parameters are independent, but you can constrain each one separately. The caller must supply all type arguments, and the compiler enforces that each argument satisfies its corresponding constraint.
Constraints on Generic Type Parameters
Without constraints, a generic type parameter can be any type. Often, however, you need to guarantee that the type has certain members, inherits from a base class, or implements an interface. Constraints are declared with the where keyword. Common constraints include where T : class (reference type), where T : struct (value type), where T : new() (parameterless constructor), and where T : BaseClass or where T : ISomeInterface.
public class Factory<T> where T : IProduct, new() { public T Create() { return new T(); } }
This constraint ensures that T implements IProduct and has a public parameterless constructor, so the new T() expression is valid. Without the new() constraint, the compiler would reject new T() because not all types have a default constructor. Constraints also let you call methods defined on the base type or interface, because the compiler knows T has those members.
Variance in Generic Interfaces and Delegates
Variance controls whether a generic type parameter can be used as an input, an output, or both. On interfaces and delegates, you can mark a type parameter with in (contravariance) or out (covariance). Variance lets a generic interface be assigned to another with a different type argument when the type arguments are related by inheritance.
public interface IProducer<out T> { T Produce(); } public interface IConsumer<in T> { void Consume(T item); }
out T means the type parameter appears only in output positions, such as return values. An IProducer<Cat> can be assigned to IProducer<Animal> because a producer of cats is also a producer of animals. Conversely, in T means the type parameter appears only in input positions, such as method arguments. An IConsumer<Animal> can be assigned to IConsumer<Cat> because a consumer that accepts any animal can also accept a cat. Variance does not apply to classes, only to interfaces and delegates, and it requires the type parameter to be used strictly in the allowed positions.
Runtime Behavior and Performance Considerations
Generic type arguments are supplied at compile time, but runtime behavior depends on whether the type argument is a reference type or a value type. For reference types, the JIT compiler can share a single native code implementation because all references have the same representation. For value types, each distinct value type gets a specialized implementation. As a result, List<int> stores integers without boxing, while a non-generic ArrayList boxes each integer. Avoiding boxing reduces memory allocations and garbage collection pressure, which helps in high-throughput code.
Generic code can have a small overhead compared with a hand-written non-generic version, mainly because of type metadata indirection, but in most applications this overhead is negligible. The larger performance risk is misuse: invoking generic methods through reflection with runtime-only types, or creating many closed types with value type arguments. If you need to call a generic method with a type known only at runtime, reflection is required and is significantly slower. In that situation, consider an interface-based design or a non-generic fallback.
Common Mistakes and How to Avoid Them
One frequent mistake is assuming that a generic type parameter supports operators such as + or == without constraints. The compiler cannot know whether T implements those operators, so you cannot write T result = left + right; directly. Provide a delegate or interface that supplies the operation, or use dynamic and lose compile-time type safety.
Another mistake is forgetting the new() constraint before instantiating a type parameter. The compiler rejects new T() unless T is constrained to have a public parameterless constructor. Similarly, you cannot use == to compare two values of an unconstrained T; use EqualityComparer<T>.Default.Equals instead.
A third issue is over-constraining. Unnecessary constraints reduce the reusability of a generic type. For example, requiring where T : class when you only need to store the value prevents value types from being used. Add only constraints that the operations you perform actually require.
When to Prefer Generic Type Parameters Over Other Approaches
Generic type parameters are the right choice when you need compile-time type safety and want to avoid casting or boxing. They suit collection classes, algorithms over sequences, and factory patterns where the caller determines the concrete type. If you only need to handle a small, fixed set of types, a non-generic interface or abstract base class may be simpler and easier to maintain.
C# attributes cannot be generic, so an attribute must use a non-generic design or accept a System.Type parameter. Another consideration is binary compatibility. Once you publish a generic type, changing its constraints or the number of type parameters is a breaking change. Design the signature carefully before releasing it. For internal code you can refactor freely, but public APIs benefit from stability. If you anticipate changing constraints later, consider a non-generic base interface with a generic implementation so the public surface remains flexible.