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 — расп