C# Generic Type Parameter: Syntax and Constraints
c# generic type parameter: Learn how C# generic type parameters work, including syntax, constraints, variance, and runtime behavior for type-safe reusable code.
c# generic type parameter requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
In C#, 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. Instead of writing separate implementations for each type, you declare a type parameter and let the caller specify the concrete type at compile time. This mechanism is central to many .NET collections and utility methods, and understanding it is essential for writing reusable, maintainable code.
Declaring a Generic Type Parameter
A generic type parameter is introduced using angle brackets after the type or method name. For a class, you 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>(). The type parameter can be used inside the class or method as if it were a concrete type, 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]; } }
Here, T is the generic type parameter. When you instantiate Repository<int>, every 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 and methods.
Using Multiple Type Parameters
A generic type can declare more than one type parameter. This is common for key-value pairs, where two independent types are needed. The syntax is straightforward: Dictionary<TKey, TValue> is a built-in example. When you define your own, list the parameters separated by commas.
public class Pair<TFirst, TSecond> { public TFirst First { get; set; } public TSecond Second { get; set; } }
Multiple parameters are useful when the relationship between types matters, such as a mapper that converts an input to an output. The type parameters are independent, but you can apply constraints to 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. But often you need to guarantee that the type has certain members or inherits from a base class. 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 enable you to 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. In C#, you can mark a type parameter with in (contravariance) or out (covariance) on interfaces and delegates. This allows a generic interface to be assigned to another with a different type argument, provided the types 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 (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 (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 parameters are resolved at compile time, but the runtime behavior depends on whether the type argument is a reference type or a value type. For reference types, the JIT compiler shares a single native code implementation because all references have the same representation. For value types, each distinct value type gets its own specialized implementation. This means a List<int> does not box integers when storing them, whereas a non-generic ArrayList would box every integer. Avoiding boxing reduces memory allocations and garbage collection pressure, which is a measurable performance benefit in high-throughput code.
However, generic code can have a small overhead compared to a hand-written non-generic version, mainly due to the indirection of type metadata. In practice, this overhead is negligible for most applications. The larger performance risk comes from misuse, such as using reflection to invoke generic methods dynamically or creating excessive closed types with many value type arguments. If you need to call a generic method with a type known only at runtime, you must use reflection, which incurs a significant cost. In that scenario, 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 can be used with operators like + or == without constraints. The compiler cannot know whether T supports those operators, so you cannot write T result = left + right; directly. You need to provide a delegate or an interface that supplies the operation, or use dynamic (which loses type safety).
Another mistake is forgetting the new() constraint when trying to instantiate a type parameter. The compiler will reject new T() unless you explicitly constrain T to have a parameterless constructor. Similarly, if you need to compare two values of type T, you cannot use == unless you constrain T to a known type or use EqualityComparer<T>.Default, which handles nulls and value types correctly.
A third issue is over-constraining. Adding unnecessary constraints reduces the reusability of your generic type. For example, requiring where T : class when you only need to store the value prevents value types from being used. Only add constraints that are strictly required for the operations you perform.
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 are ideal for collection classes, algorithms that work on sequences, and factory patterns where the concrete type is determined by the caller. If you only need to handle a small, fixed set of types, a non-generic interface or an abstract base class may be simpler and easier to maintain. Generics also cannot be used in certain contexts, such as attributes, where the type argument must be a compile-time constant. In those cases, you must fall back to non-generic code or pass the type as a 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 generic signature carefully before releasing it. For internal code, you can refactor freely, but for public APIs, stability matters. If you anticipate that the type parameter may need to be constrained differently in the future, consider using a non-generic base interface and a generic implementation, so the public surface remains flexible.