Что такое обобщения (Generics) в C#

Ответ

Обобщения (Generics) — это механизм в C#, позволяющий создавать типобезопасные классы, структуры, интерфейсы и методы, которые работают с данными произвольного типа, указанного во время использования, а не во время объявления. Это устраняет необходимость приведения типов (casting) и проверок во время выполнения, перенося эти проверки на этап компиляции.

Основная идея: Параметризация типов. Вы пишете алгоритм или структуру данных один раз, но указываете конкретный тип, с которым будете работать, позже.

Базовый пример: обобщенный класс Repository<T>

// Объявление обобщенного класса с параметром типа T
public class Repository<T> where T : class, IEntity, new() // Ограничения (constraints)
{
    private readonly List<T> _items = new();

    public void Add(T entity)
    {
        _items.Add(entity);
    }

    public T GetById(int id)
    {
        // Компилятор знает, что T имеет свойство Id (благодаря ограничению IEntity)
        return _items.FirstOrDefault(e => e.Id == id);
    }

    public T CreateNew()
    {
        return new T(); // Возможно благодаря ограничению 'new()'
    }
}

// Использование с конкретными типами
var userRepo = new Repository<User>(); // T = User
userRepo.Add(new User { Id = 1, Name = "Alice" });
User foundUser = userRepo.GetById(1); // Без приведения типа! Тип возврата - User.

var productRepo = new Repository<Product>(); // T = Product
Product newProduct = productRepo.CreateNew();

Обобщенные методы:

// Метод, работающий с массивом любого типа
public static T FindMax<T>(T[] array) where T : IComparable<T>
{
    if (array == null || array.Length == 0)
        throw new ArgumentException("Array is empty or null");

    T max = array[0];
    for (int i = 1; i < array.Length; i++)
    {
        // Сравнение возможно благодаря IComparable<T>
        if (array[i].CompareTo(max) > 0)
            max = array[i];
    }
    return max; // Тип возврата - T
}

// Вызов
int maxInt = FindMax(new[] { 1, 5, 3, 9, 2 }); // T выводится как int
string maxString = FindMax(new[] { "apple", "zebra", "banana" }); // T = string

Ключевые преимущества:

  1. Безопасность типов (Type Safety): Компилятор проверяет соответствие типов. Исключаются ошибки InvalidCastException во время выполнения.

    // Без Generics (старый ArrayList) - ОПАСНО
    ArrayList list = new ArrayList();
    list.Add(42); // boxed to object
    list.Add("oops"); // тоже object
    int num = (int)list[1]; // Runtime InvalidCastException!
    
    // С Generics (List<T>) - БЕЗОПАСНО
    List<int> safeList = new List<int>();
    safeList.Add(42);
    // safeList.Add("oops"); // ОШИБКА КОМПИЛЯЦИИ: нельзя добавить string в List<int>
    int safeNum = safeList[0]; // Приведение не требуется
  2. Производительность:

    • Для значимых типов (value types) исключаются операции упаковки (boxing) и распаковки (unboxing), которые требуют выделения памяти в куче и снижают производительность.
    • Код для конкретных типов генерируется во время JIT-компиляции, что приводит к высокооптимизированному машинному коду.
  3. Повторное использование кода и читаемость: Один обобщенный алгоритм (например, сортировка, поиск) или структура данных (список, словарь) может использоваться с любым типом. Код становится чище и понятнее, так как исчезают приведения типов и object.

Ограничения (Constraints): Позволяют указать требования к параметру типа T.

public class Processor<T> where T : 
    class,        // T должен быть ссылочным типом
    IDisposable,  // T должен реализовывать интерфейс IDisposable
    new()         // T должен иметь публичный конструктор без параметров
{
    public void Process()
    {
        T item = new T(); // Можно создать экземпляр
        using (item)      // Можно использовать в using, т.к. IDisposable
        {
            // ... работа с item
        }
    }
}

Важные встроенные обобщенные типы в .NET: List<T>, Dictionary<TKey, TValue>, Nullable<T>, Task<TResult>, все интерфейсы коллекций (IEnumerable<T>, ICollection<T>).

Ответ 18+ 🔞

Ну ты смотри, какие штуки придумали, блядь! Обобщения, ёпта, в C# — это ж просто магия какая-то, а не фича. Суть в чём, понимаешь? Чтобы не писать один и тот же код для каждого типа отдельно, как последний лох, а написать один раз и чтобы работало со всеми.

Представь, ты делаешь хранилище, контейнер, там, список какой-нибудь. Раньше, в старые добрые времена, делали так: ArrayList, блядь. Туда можно было запихнуть всё подряд: число, строку, объект, соседского кота. А когда достаёшь — охуеваешь, потому что надо кастить, и если ошибся — получаешь InvalidCastException прямо в ебало во время работы программы. Пиздец, а не разработка.

// Вот этот пиздец, который все помнят
ArrayList старыйГовноСписок = new ArrayList();
старыйГовноСписок.Add(42); // Число — окей
старыйГовноСписок.Add("Васёк"); // Строка — тоже окей, блядь
int число = (int)старыйГовноСписок[1]; // Runtime: "Э, сука, да это же строка!" — ПИЗДЕЦ.

А теперь смотри, как это делают нормальные люди с дженериками:

// Объявляешь класс один раз, а тип `T` — это как заглушка, которую подставишь потом
public class УмноеХранилище<T> where T : class, IEntity, new() // Вот эти `where` — ограничения, чтобы `T` не был левым
{
    private List<T> _items = new();

    public void Добавить(T штука)
    {
        _items.Add(штука); // Всё типобезопасно, компилятор следит
    }

    public T СоздатьНовую()
    {
        return new T(); // И даже создать можно, если в ограничениях `new()` стоит
    }
}

// А используешь вот так — красота, блядь!
var хранилищеПользователей = new УмноеХранилище<Пользователь>();
хранилищеПользователей.Добавить(new Пользователь());
// хранилищеПользователей.Добавить(new Товар()); // А вот так НЕЛЬЗЯ! Компилятор сразу наорет: "Куда, сука, лезешь? Тут тип `Пользователь`!"
Пользователь юзер = хранилищеПользователей.СоздатьНовую(); // И кастить ничего не надо, всё на месте.

И методы такие же гибкие можно делать. Вот, например, метод поиска максимума в массиве. Раньше для int один, для double другой, для string третий... Да заебёшься же, в рот меня чих-пых!

public static T НайтиМаксимум<T>(T[] массив) where T : IComparable<T> // Говорим: "Т, будь добр, умей сравнивать сам с собой"
{
    if (массив == null || массив.Length == 0)
        throw new ArgumentException("Массив-то пустой, мудак!");

    T максимум = массив[0];
    for (int i = 1; i < массив.Length; i++)
    {
        if (массив[i].CompareTo(максимум) > 0) // Всё работает, потому что T сравниваемый
            максимум = массив[i];
    }
    return максимум;
}

// Используешь — и не паришься
int максЧисло = НайтиМаксимум(new[] { 1, 5, 3, 9, 2 }); // Компилятор сам догадывается, что T — это int
string максСтрока = НайтиМаксимум(new[] { "яблоко", "жираф", "банан" }); // А тут T — string

А главные плюсы, блядь, какие?

  1. Безопасность типов. Ошибки ловятся на компиляции, а не в рантайме, когда уже пользователю светят. Компилятор — строгий такой охранник, который не пропустит левую хуйню.
  2. Производительность. Особенно для структур (int, double, DateTime). Раньше их в object запаковывали (boxing), память лишнюю жрали, скорость падала. А теперь дженерики генерируют код под конкретный тип, и всё летает. Никакого боксинга, чистая скорость.
  3. Переиспользование кода и читаемость. Один алгоритм — для всех типов. Код чище, приведения типов не торчат, как гвозди из стула, на который сесть пытаешься.

Ограничения (where) — это вообще мощнейшая вещь. Ты можешь сказать: "Т, слушай сюда, ты должен быть ссылочным типом (class), должен уметь уничтожаться (IDisposable) и иметь конструктор без параметров (new())". И всё, компилятор тебя заставит это соблюдать.

public class Обработчик<T> where T : class, IDisposable, new()
{
    public void Обработать()
    {
        T штука = new T(); // Можно создать
        using (штука)      // Можно в using запихнуть, потому что он IDisposable
        {
            // ... делаем что-то
        }
    }
}

В общем, вся стандартная библиотека .NET на этом стоит: List<T>, Dictionary<TKey, TValue>, Task<TResult>. Без дженериков это была бы одна большая помойка с приведениями типов. Так что учи, применяй — и будешь жить, как человек, а не как мазохист, который всё вручную проверяет.

Видео-ответы