Generics в C#: полное руководство по использованию и преимуществам

Онлайн-тренажер Python для начинающих

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

Начать курс

Generics в C#: зачем нужны и как использовать

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

Зачем нужны Generics в C#

До появления Generics в C# (версия 2.0) разработчики часто использовали тип object и коллекции ArrayList. Это приводило к двум проблемам:

  • Потеря производительности — упаковка (boxing) и распаковка (unboxing) для значимых типов (int, double, struct).
  • Отсутствие проверки типов на этапе компиляции — можно случайно положить строку в список чисел и получить ошибку только в рантайме.

Generics решают обе проблемы. Они позволяют создавать типобезопасные структуры данных без лишних накладных расходов. Например, List<int> работает с int без упаковки, а компилятор не даст добавить туда строку.

Основные понятия Generics

Обобщенные классы

Generic-класс объявляется с параметром типа в угловых скобках. Параметр типа — это placeholder, который заменяется реальным типом при создании экземпляра.

public class Box<T>
{
    private T _value;

    public Box(T value)
    {
        _value = value;
    }

    public T GetValue() => _value;
}

Использование:

var intBox = new Box<int>(42);
var stringBox = new Box<string>("Hello");

Console.WriteLine(intBox.GetValue());   // 42
Console.WriteLine(stringBox.GetValue());// Hello

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

Метод может иметь свой собственный параметр типа, даже если класс не является generic:

public class Utils
{
    public static void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }
}

// Использование
int x = 1, y = 2;
Utils.Swap(ref x, ref y);
Console.WriteLine($"{x} {y}"); // 2 1

Обобщенные интерфейсы

Интерфейсы с параметром типа позволяют создавать гибкие абстракции:

public interface IRepository<T>
{
    T GetById(int id);
    void Add(T entity);
}

public class UserRepository : IRepository<User>
{
    public User GetById(int id) => new User();
    public void Add(User user) { }
}

Ограничения (Constraints) в Generics

Иногда нужно наложить ограничения на типы, которые могут быть подставлены. Для этого используется ключевое слово where.

Основные виды ограничений

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

Пример с несколькими ограничениями:

public class Repository<T> where T : class, IEntity, new()
{
    public T Create()
    {
        return new T(); // разрешено благодаря new()
    }
}

Covariance и Contravariance в Generics

Начиная с C# 4.0, появилась возможность управлять вариантностью generic-интерфейсов с помощью ключевых слов in и out.

  • Ковариантность (out) — позволяет использовать более производный тип, чем указан изначально. Применяется для возвращаемых значений.
  • Контравариантность (in) — позволяет использовать более базовый тип. Применяется для входных параметров.

Пример ковариантности:

public interface IProducer<out T>
{
    T Produce();
}

// IEnumerable<T> — ковариантен
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // работает

Пример контравариантности:

public interface IConsumer<in T>
{
    void Consume(T item);
}

// IComparer<T> — контравариантен
IComparer<object> objectComparer = new MyComparer();
IComparer<string> stringComparer = objectComparer; // работает

Generics и производительность

Одно из главных преимуществ Generics — отсутствие упаковки (boxing) для значимых типов. Сравните:

// Без Generics (ArrayList)
var list = new ArrayList();
list.Add(42);           // boxing — int упаковывается в object
int val = (int)list[0]; // unboxing — распаковка

// С Generics (List<T>)
var list2 = new List<int>();
list2.Add(42);          // нет boxing
int val2 = list2[0];    // нет unboxing

При работе с сотнями тысяч элементов разница в производительности становится критичной. Для структур (struct) Generics также позволяют избежать копирования при передаче в методы.

Практические примеры использования

Создание собственной коллекции

public class MyStack<T>
{
    private T[] _items;
    private int _count;

    public MyStack(int capacity = 10)
    {
        _items = new T[capacity];
    }

    public void Push(T item)
    {
        if (_count == _items.Length)
            Array.Resize(ref _items, _items.Length * 2);
        _items[_count++] = item;
    }

    public T Pop()
    {
        if (_count == 0)
            throw new InvalidOperationException("Stack is empty");
        return _items[--_count];
    }
}

Generic-метод для поиска в массиве

public static int FindIndex<T>(T[] array, T value) where T : I                

Блоги

Рекомендаций по книгам