Дженерики дают типобезопасный код без упаковки. На собесе спрашивают ограничения, вариантность и то, как это работает в рантайме.
Зачем
ArrayList хранит object: числа упаковываются, а ошибки типов ловятся только во время выполнения. List<int> хранит int[], проверка типа на этапе компиляции, упаковки нет.
static T Max<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b;
Ограничения where
class,struct,notnull,unmanaged;new(): можно писатьnew T();- базовый класс или интерфейс: доступны его члены;
where T : U: связь двух параметров.
Без ограничений с T можно делать только то, что умеет object. С .NET 7 есть обобщённая математика: where T : INumber<T> позволяет писать a + b и T.Zero, это работает на статических абстрактных членах интерфейсов (C# 11).
default(T)
default даёт null для ссылочных типов и обнулённое значение для типов значений. Конструктор не вызывается.
Статика в дженериках
Статические поля и статический конструктор свои у каждого закрытого типа: Cache<int> и Cache<string> не делят данные. На этом строят быстрые кеши «на тип»:
static class TypeInfo<T> { public static readonly string Name = typeof(T).Name; }
Вариантность
Работает только для интерфейсов и делегатов и только со ссылочными типами.
- Ковариантность
out T: тип только на выходе.IEnumerable<string>можно присвоитьIEnumerable<object>. - Контравариантность
in T: тип только на входе.Action<object>можно использовать какAction<string>,IComparer<Animal>какIComparer<Cat>. IList<T>иList<T>инвариантны: через них можно писать, и подмена типа нарушила бы безопасность.
Массивы ковариантны исторически: object[] a = new string[1]; a[0] = 1; скомпилируется и упадёт с ArrayTypeMismatchException.
Как это работает в рантайме
Компилятор C# генерирует один IL с параметрами-типами. JIT создаёт отдельный машинный код для каждого типа значения (List<int>, List<double>), а для всех ссылочных типов общий код. Это отличие от шаблонов C++, где копии создаёт компилятор.
Практика: обобщённые структуры данных
class MinStack<T> where T : IComparable<T>
{
private readonly Stack<(T Value, T Min)> _s = new();
public void Push(T x) =>
_s.Push((x, _s.Count == 0 || x.CompareTo(_s.Peek().Min) < 0 ? x : _s.Peek().Min));
public T Min => _s.Peek().Min;
}
Частые ошибки
- Ожидать вариантность у
List<T>или дляint. - Забыть ограничение и пытаться вызвать метод, которого нет у
object. - Делать «обобщённый репозиторий», который протекает
IQueryableи прячет смысл запросов.
Что спросят на собесе
- Зачем нужны дженерики и чем
List<int>лучшеArrayList? - Какие бывают ограничения
where? - Что такое ковариантность и контравариантность, где они в BCL?
- Как JIT компилирует дженерики для типов значений и ссылочных?
- Чем статическое поле в
Foo<int>отличается от поля вFoo<string>?