Что такое 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