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