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

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

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

Начать курс

Что такое Generics в C# и зачем они нужны

Представьте, что вы пишете метод, который должен работать с разными типами данных: целыми числами, строками, объектами. Без Generics (обобщений) вам пришлось бы либо дублировать код для каждого типа, либо использовать object с последующим приведением типов. Оба подхода приводят к проблемам: первый — к избыточности, второй — к ошибкам времени выполнения.

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

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

  • Безопасность типов — компилятор проверяет корректность использования типов на этапе компиляции
  • Производительность — отсутствие упаковки/распаковки (boxing/unboxing) для значимых типов
  • Переиспользование кода — один класс/метод работает с множеством типов
  • Читаемость — код становится чище и понятнее

Проблема без Generics: пример на object

До появления Generics разработчики часто использовали тип object для создания универсальных методов. Рассмотрим пример:

public class Stack
{
    private object[] _items = new object[10];
    private int _count = 0;
    
    public void Push(object item) { _items[_count++] = item; }
    public object Pop() { return _items[--_count]; }
    
    // Использование
    Stack stack = new Stack();
    stack.Push(123);           // Упаковка int в object
    stack.Push("hello");       // Любой тип можно добавить
    int number = (int)stack.Pop(); // Распаковка и приведение
}

Проблемы очевидны:

  • При извлечении нужно явное приведение типа
  • Можно случайно добавить значение другого типа
  • Для значимых типов (int, double) происходит упаковка/распаковка, что снижает производительность

Основы работы с Generics в C#

Создание обобщенного класса

Рассмотрим тот же пример стека, но с использованием Generics:

public class Stack<T>
{
    private T[] _items = new T[10];
    private int _count = 0;
    
    public void Push(T item) { _items[_count++] = item; }
    public T Pop() { return _items[--_count]; }
}

// Использование
Stack<int> intStack = new Stack<int>();
intStack.Push(123);
intStack.Push(456);
int number = intStack.Pop(); // Распаковка не нужна, тип известен

Stack<string> stringStack = new Stack<string>();
stringStack.Push("Hello");
// stringStack.Push(123); // Ошибка компиляции! Безопасность типов

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

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

Generics работают не только с классами, но и с отдельными методами:

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

// Использование
int maxInt = Utils.Max(5, 10);           // 10
string maxStr = Utils.Max("apple", "banana"); // "banana"

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

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

// Ограничение: T должен быть ссылочным типом
public class Repository<T> where T : class
{
    public void Save(T entity) { /* сохранение */ }
}

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

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

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

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

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

Коллекции в .NET

Самый распространенный пример — коллекции из пространства имен System.Collections.Generic:

// Список строк
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
names.Add("David");

// Словарь с ключом-строкой и значением-целым числом
Dictionary<string, int> ages = new Dictionary<string, int>
{
    { "Alice", 30 },
    { "Bob", 25 }
};

// Очередь с проверкой на null
Queue<string?> messages = new Queue<string?>();

Создание универсального репозитория

В реальных проектах Generics часто используются для создания абстрактных слоев доступа к данным:

public interface IRepository<T> where T : class
{
    T GetById(int id);
    IEnumerable<T> GetAll();
    void Add(T entity);
    void Update(T entity);
    void Delete(int id);
}

public class Repository<T> : IRepository<T> where T : class
{
    private readonly DbContext _context;
    private readonly DbSet<T> _dbSet;
    
    public Repository(DbContext context)
    {
        _context = context                

Блоги

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