Skip to main content
Generic Constraints are rules applied to type parameters (T) in a generic class, struct, interface, or method. They restrict the kinds of types that can be used for that parameter. Without constraints, T is treated as an object, limiting you to only the most basic operations.

Reference/Value Types

These constraints restrict the generic type parameter to be either a reference type (class) or a non-nullable value type (struct).
  • where T : class: Ensures T is a reference type (e.g., a class, string, array, delegate, or interface)
  • where T : struct: Ensures T is a non-nullable value type (e.g., int, DateTime, or a custom struct)
// Example of 'class' and 'struct' constraints
public class ReferenceHandler\<T\> where T : class
{
    public void SetToNull(T item)
    {
        item = null; // This is legal because T is guaranteed to be a reference type.
    }
}

public struct ValueContainer\<T\> where T : struct
{
    public T Value { get; set; }
    // T cannot be null here, which guarantees Value has a valid default.
}

// Usage
var handler = new ReferenceHandler<string>(); // Valid: string is a reference type
// var invalidHandler = new ReferenceHandler<int>(); // COMPILER ERROR: int is a struct

var container = new ValueContainer<int>(); // Valid: int is a struct
// var invalidContainer = new ValueContainer<string>(); // COMPILER ERROR: string is a class
Use Case: where T : class allows assigning null to variables of type T. where T : struct ensures value semantics and prevents nullable types.

New Constructor

This constraint requires that the type T must have a public parameterless constructor. It enables you to create new instances of T inside your generic code using new T().
  • where T : new(): The type T must have a public parameterless constructor
// Example of the 'new' constraint
public class Repository\<T\> where T : new()
{
    public T CreateNewEntity()
    {
        return new T(); // This is only allowed because of the 'new()' constraint.
    }
}

public class Customer
{
    // An implicit public parameterless constructor exists.
    public string Name { get; set; }
}

public class Order
{
    public Order(int orderId) { } // No parameterless constructor exists.
}

// Usage
var customerRepo = new Repository<Customer>(); // Valid: Customer has a parameterless ctor.
Customer newCustomer = customerRepo.CreateNewEntity();

// var orderRepo = new Repository<Order>(); // COMPILER ERROR: Order lacks a parameterless ctor.
Constructor Requirement: The new() constraint requires a parameterless constructor. Classes with only parameterized constructors will fail compilation.

Interface Constraint

This is one of the most common and powerful constraints. It requires that T must implement a specific interface. This allows you to call methods and properties defined by that interface on variables of type T.
  • where T : IComparable: Ensures any type substituted for T must implement the IComparable interface
// Example of an interface constraint
public class Sorter\<T\> where T : IComparable\<T\>
{
    public void BubbleSort(T[] array)
    {
        for (int i = 0; i < array.Length; i++)
        {
            for (int j = i + 1; j < array.Length; j++)
            {
                // We can use CompareTo because T is guaranteed to be IComparable\<T\>.
                if (array[i].CompareTo(array[j]) > 0)
                {
                    // Swap elements
                    T temp = array[i];
                    array[i] = array[j];
                    array[j] = temp;
                }
            }
        }
    }
}

// Usage
int[] numbers = { 5, 2, 8, 1 };
var intSorter = new Sorter<int>(); // Valid: int implements IComparable<int>
intSorter.BubbleSort(numbers);

string[] names = { "Charlie", "Alice", "Bob" };
var stringSorter = new Sorter<string>(); // Valid: string implements IComparable<string>
stringSorter.BubbleSort(names);
Best Practice: Interface constraints enable compile-time guarantees about the capabilities of type T, moving errors from runtime to compile-time.

Why Generic Constraints are Important

  1. Type Safety (Fail-Fast): Constraints move potential runtime errors to compile-time
  2. Design for Intent (SOLID): Clearly communicate the minimal capabilities your generic code requires
  3. Elimination of Boxing: Using where T : struct with collections avoids boxing overhead

Advanced Nuances

Combining Multiple Constraints

You can apply several constraints to a single type parameter. The order is strict: class/struct must come first, then any base class, then interfaces, and finally new().
public class AdvancedRepository\<T\> where T : class, IEntity, new()
{
    // T must be a reference type, implement IEntity, and have a parameterless ctor.
}

Covariance and Contravariance with Interface Constraints

If you constrain T to a variant interface like IEnumerable<out T>, you can leverage covariance:
// Method accepting IEnumerable<Animal> can be called with IEnumerable<Cat>
void ProcessAnimals\<T\>(IEnumerable\<T\> animals) where T : Animal
{
    // Covariance enables flexible type substitution
}

The default Keyword and struct Constraint

With where T : struct, default(T) returns the zero-initialized value of the struct:
public T GetDefaultValue\<T\>() where T : struct
{
    return default(T); // Returns 0 for int, DateTime.MinValue for DateTime, etc.
}

Roadmap Context

Generic Constraints are a fundamental pillar within the “Generics and Collections” section. Mastery of constraints is a prerequisite for:
  • Understanding how List\<T\> efficiently stores both reference and value types
  • Building custom, type-safe collections
  • Implementing Repository and Specification patterns
  • Understanding how IoC containers use constraints for dependency resolution

Build docs developers (and LLMs) love