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

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

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

Начать курс

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

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

Что такое Generics и зачем они нужны?

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

// Без Generics — дублирование кода
void SwapInt(ref int a, ref int b) { int temp = a; a = b; b = temp; }
void SwapString(ref string a, ref string b) { string temp = a; a = b; b = temp; }
// ... и так для каждого типа

С Generics эта проблема решается элегантно:

// С Generics — один метод для всех типов
void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

// Использование
int x = 1, y = 2;
Swap(ref x, ref y); // T = int

string s1 = "Hello", s2 = "World";
Swap(ref s1, ref s2); // T = string

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

  • Типобезопасность — ошибки типов отлавливаются на этапе компиляции
  • Производительность — отсутствие упаковки/распаковки для значимых типов
  • Переиспользование кода — один класс/метод работает с любыми типами
  • Читаемость — код становится более понятным и лаконичным

Generic-классы и структуры

Самый частый пример использования Generics — это коллекции. Вместо того чтобы писать свой класс для каждого типа, мы создаём обобщённый:

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

    public MyList(int capacity = 4)
    {
        _items = new T[capacity];
        _count = 0;
    }

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

    public T Get(int index)
    {
        if (index < 0 || index >= _count)
            throw new IndexOutOfRangeException();
        return _items[index];
    }

    public int Count => _count;
}

// Использование
var numbers = new MyList<int>();
numbers.Add(10);
numbers.Add(20);

var names = new MyList<string>();
names.Add("Alice");

Обратите внимание: при создании экземпляра мы указываем конкретный тип в угловых скобках <T>. Компилятор подставляет этот тип во все места, где используется T.

Generic-методы и ограничения (constraints)

Иногда нужно наложить ограничения на тип T. Например, чтобы гарантировать, что тип реализует определённый интерфейс или имеет конструктор без параметров:

// Ограничение: T должен реализовывать IComparable<T>
public T GetMax<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

// Ограничение: T должен иметь конструктор без параметров
public class Factory<T> where T : new()
{
    public T Create()
    {
        return new T();
    }
}

// Ограничение: T должен быть значимым типом (struct)
public struct Nullable<T> where T : struct
{
    private T _value;
    private bool _hasValue;

    public Nullable(T value)
    {
        _value = value;
        _hasValue = true;
    }

    public T Value => _hasValue ? _value : throw new InvalidOperationException();
    public bool HasValue => _hasValue;
}

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

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

Практический пример: репозиторий с Generics

Рассмотрим реальный сценарий — создание обобщённого репозитория для работы с базой данных. Это типичная задача в разработке на C#:

public interface IEntity
{
    int Id { get; set; }
}

public class GenericRepository<T> where T : class, IEntity
{
    private readonly List<T> _context = new List<T>();

    public T GetById(int id) => _context.FirstOrDefault(e => e.Id == id);

    public IEnumerable<T> GetAll() => _context.ToList();

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

    public void Update(T entity)
    {
        var existing = GetById(entity.Id);
        if (existing != null)
        {
            _context.Remove(existing);
            _context.Add(entity);
        }
    }

    public void Delete(int id)
    {
        var entity = GetById(id);
        if (entity != null)
            _context.Remove(entity);
    }
}

// Использование
public class User : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

public class Product : IEntity
{
    public int Id { get; set; }
    public string Title { get; set; }
    public decimal Price { get; set; }
}

// Работаем с разными сущностями через один класс
var userRepo = new GenericRepository<User>();
userRepo.Add(new User { Id = 1, Name = "Иван", Email = "ivan@mail.ru" });

var productRepo = new GenericRepository<Product>();
productRepo.Add(new Product { Id = 1, Title = "Ноутбук", Price = 999.99m });

Этот пример наглядно демонстрирует, как Generics позволяют избежать дублирования кода для разных типов сущностей, сохраняя при этом полную типобезопасность.

Производительность Generics vs object

Многие разработчики, знакомые с более старыми версиями C#, помнят времена, когда коллекции работали с object. Сравним производительность:

// Старый подход с object (boxing/unboxing)
ArrayList list = new ArrayList();
list.Add(42); // boxing — упаковка int в object
int value = (int)list[0]; // unboxing — расп                

Блоги

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