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

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

Ответ

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

Основные типы ограничений:

Ограничение Синтаксис Описание Пример использования
Класс where T : class T должен быть ссылочным типом (class, interface, delegate, array). T instance = null; (допустимо присваивание null)
Структура where T : struct T должен быть значимым типом (не-nullable struct). T value = default; (получаем значение по умолчанию, не null)
Конструктор where T : new() T должен иметь открытый конструктор без параметров. T obj = new T();
Базовый класс where T : BaseClass T должен наследоваться от указанного класса. Можно вызывать методы BaseClass.
Интерфейс where T : ISomeInterface T должен реализовывать указанный интерфейс. Можно вызывать методы ISomeInterface.
Универсальный делегат where T : Delegate (C# 7.3+) T должен быть типом делегата. Полезно для кэширования или комбинации делегатов.
Enum where T : Enum (C# 7.3+) T должен быть типом перечисления. Работа с флагами, валидация значений.
Unmanaged where T : unmanaged (C# 7.3+) T должен быть неуправляемым типом (примитивы, struct без ссылочных полей). Низкоуровневые операции, указатели, межплатформенное взаимодействие.

Примеры комбинирования ограничений:

// T должен быть классом, реализующим IComparable, и иметь конструктор по умолчанию
public class SortedRepository<T> where T : class, IComparable<T>, new()
{
    private List<T> _items = new();

    public T AddNew()
    {
        var newItem = new T(); // Возможно благодаря 'new()'
        _items.Add(newItem);
        _items.Sort();         // Возможно благодаря 'IComparable<T>'
        return newItem;
    }
}

// T должен быть значимым типом и реализовывать интерфейсы
public T Max<T>(T a, T b) where T : struct, IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b; // Сравнение возможно благодаря IComparable<T>
}

// Ограничение на несколько типов (C# 11+ с 'and' не поддерживается, но можно через интерфейсы)
// Часто используется комбинация class + интерфейс(ы) + new()
public class Factory<T> where T : IDisposable, ICloneable, new()
{
    public T CreateAndSetup()
    {
        var obj = new T();
        // Настройка obj...
        return obj;
    }
}

Ограничения в методах:

// Ограничения можно применять и к методам
public static T DeserializeFromJson<T>(string json) where T : new()
{
    var result = new T();
    // ... логика десериализации (например, через рефлексию)
    return result;
}

Практическая польза:

  1. Безопасность типов: Компилятор предотвращает передачу некорректных типов.
  2. Улучшенный IntelliSense: Среда разработки "знает" возможности типа T.
  3. Производительность: Позволяет избежать боксинга для where T : struct и использовать более эффективные операции.