Как задать ограничение (constraint) на тип для параметра обобщения (generic) в C#?

«Как задать ограничение (constraint) на тип для параметра обобщения (generic) в C#?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Ограничения обобщенных типов задаются с помощью ключевого слова where. Они указывают компилятору, какие типы могут быть использованы в качестве аргументов типа, и предоставляют дополнительную информацию о этих типах внутри обобщенного кода.

Синтаксис и виды ограничений:

public class Repository<T> where T : class, IEntity, new()
{
    // T должен быть ссылочным типом (class),
    // реализовывать интерфейс IEntity,
    // и иметь открытый конструктор без параметров (new()).
}
Ограничение Описание Пример использования
where T : struct T должен быть значимым типом (не-nullable). T может быть int, DateTime, Guid. Не может быть string или class.
where T : class T должен быть ссылочным типом. T может быть string, List<int>, любой пользовательский class.
where T : notnull (C# 8.0+) T должен быть типом, не допускающим null. Запрещает T? для ссылочных типов. Полезно в контексте nullable reference types.
where T : unmanaged (C# 7.3+) T должен быть неуправляемым типом (не ссылочным и не содержащим ссылочных полей). T может быть int, double, struct только из примитивов. Используется для низкоуровневого кода (Span, указатели).
where T : new() T должен иметь открытый конструктор без параметров. Позволяет создавать экземпляры: T item = new T();
where T : BaseClass T должен быть или наследоваться от указанного базового класса. Гарантирует доступ к членам BaseClass.
where T : ISomeInterface T должен реализовывать указанный интерфейс. Гарантирует, что у T есть методы/свойства интерфейса.

Практические примеры:

  1. Ограничение для репозитория, работающего только с сущностями:

    public interface IEntity { int Id { get; set; } }
    public class GenericRepository<T> where T : class, IEntity
    {
        public T GetById(int id) { /* ... */ }
        // Компилятор знает, что у T есть свойство Id.
    }
  2. Ограничение для фабрики, создающей объекты:

    public static T CreateInstance<T>() where T : new()
    {
        return new T(); // Без ограничения new() эта строка не скомпилируется.
    }
  3. Ограничение для метода расширения, работающего с перечислениями:

    public static string GetDescription<T>(this T value) where T : Enum // Ограничение на Enum (C# 7.3+)
    {
        // Метод доступен только для типов перечисления.
        return typeof(T).GetField(value.ToString())?
                        .GetCustomAttribute<DescriptionAttribute>()?.Description;
    }
    // Использование: MyEnum.Value.GetDescription();
  4. Несколько параметров типа с разными ограничениями:

    public class Mapper<TFrom, TTo> 
        where TFrom : class 
        where TTo : class, new()
    {
        public TTo Map(TFrom source) { /* ... */ }
    }

Ограничения делают обобщенный код более безопасным, выразительным и производительным, предоставляя компилятору информацию для проверок и оптимизаций.