Урок по треку C# и .NET 6 мин

Дженерики

Ограничения where, ковариантность и контравариантность, статические поля в дженериках.

Дженерики дают типобезопасный код без упаковки. На собесе спрашивают ограничения, вариантность и то, как это работает в рантайме.

Зачем

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>?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.