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

Ответ

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

Основные преимущества:

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

Пример обобщённого класса:

public class Repository<T> where T : class, IEntity, new() // Ограничения
{
    private List<T> _items = new List<T>();

    public T GetById(int id) => _items.FirstOrDefault(item => item.Id == id);
    public void Add(T entity) => _items.Add(entity);
}

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

Пример обобщённого метода:

public static T Max<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

int result = Max(10, 20); // T выводится как int
string textResult = Max("abc", "xyz"); // T выводится как string

Ограничения (constraints): Позволяют указать требования к параметру типа, открывая соответствующие операции.

  • where T : struct — T должен быть значимым типом.
  • where T : class — T должен быть ссылочным типом.
  • where T : new() — T должен иметь открытый конструктор без параметров.
  • where T : BaseClass — T должен наследоваться от BaseClass.
  • where T : ISomeInterface — T должен реализовывать интерфейс ISomeInterface.

Ответ 18+ 🔞

Слушай, ну вот объясняю тебе про эти ваши обобщения, или generics, как их умные называют. Это такая штука, которая позволяет тебе не писать одно и то же говно для каждого типа отдельно. Представь, что у тебя есть коробка. И ты можешь сделать её не для конкретных носков или инструментов, а такую, в которую можно сложить что угодно — хоть гвозди, хоть бумажки, хоть свои кривые идеи. И компилятор потом сам разберётся, что там лежит, и не даст тебе воткнуть туда что попало. Красота же!

Зачем это вообще нужно, спросишь?

  1. Типы не обосрёшь. Компилятор, этот бдительный сторож, смотрит за тем, чтобы ты не сунул строку туда, где ждут число. Всё проверяется сразу, а не в момент, когда программа уже у пользователя на компе вылетает с дурацкой ошибкой.
  2. Быстро, блядь. Если работаешь с числами (int, double) или своими структурами, то никакой лишней возни с упаковкой-распаковкой. Всё летает как по маслу, прямо как будто ты для каждого типа свой отдельный, но оптимизированный код написал.
  3. Один раз написал — везде используешь. Не надо копипастить один и тот же алгоритм для int, потом для string, потом ещё для чего-нибудь. Написал один обобщённый метод — и пошёл пить чай.

Смотри, как это выглядит на практике. Вот тебе класс-хранилище:

public class Repository<T> where T : class, IEntity, new() // Это ограничения, без них никуда
{
    private List<T> _items = new List<T>();

    public T GetById(int id) => _items.FirstOrDefault(item => item.Id == id);
    public void Add(T entity) => _items.Add(entity);
}

// А используешь ты его вот так, с любым типом
var userRepo = new Repository<User>(); // Тут T — это User
var productRepo = new Repository<Product>(); // А тут T — уже Product
userRepo.Add(new User { Id = 1, Name = "Alice" }); // И всё типобезопасно, никакого приведения типов!

Или вот метод, который ищет максимум из двух значений. Раньше бы пришлось городить огород:

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

int result = Max(10, 20); // Компилятор сам догадывается, что T — это int
string textResult = Max("abc", "xyz"); // А тут T — string

Про ограничения (constraints) отдельно скажу. Это как поставить собаке намордник, чтобы она только в нужном направлении лаяла. Ты говоришь компилятору: "Смотри, тип T должен быть таким-то, иначе я с ним работать не буду".

  • where T : struct — T должен быть типом-значением (число, структура). Ссылочные сюда не пролезут.
  • where T : class — наоборот, только ссылочные типы.
  • where T : new() — у T должен быть конструктор без параметров, чтобы можно было new T() сделать.
  • where T : BaseClass — T должен быть наследником конкретного класса. Все методы базового класса будут доступны.
  • where T : ISomeInterface — T должен реализовывать этот интерфейс. Без этого — ни шагу!

Короче, generics — это мощнейшая хуйня, которая делает код и безопаснее, и быстрее, и чище. Освой её — и жизнь станет проще, ёпта.