Generics глубоко: constraints, вариантность, static abstract
↑ 2.1 C♯: продвинутый язык · ← Предыдущая · → Следующая
Зачем это на собесе
Senior-вопросы про generics: чем ковариантность отличается от контравариантности, почему
List<Dog>нельзя присвоитьList<Animal>, что такоеstatic abstractв интерфейсах и generic math, как CLR разделяет код для значимых и ссылочных типов.
Объяснение
Ограничения: что ещё умеет where
public static T Max<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b;
public class Repo<T> where T : class, IEntity, new() { } // комбинация: class + интерфейс + new()
public static T Parse<T>(string s) where T : IParsable<T> => T.Parse(s, null); // C# 11: статический интерфейсный член
public static void Fill<T>(Span<T> span) where T : unmanaged { } // без ссылок: можно stackalloc, работа с байтами
public ref struct Reader<T> where T : allows ref struct { } // C# 13: разрешить ref struct как аргумент типа- Ограничение
default— уточняет перегрузки дляT?в переопределениях. where T : notnull— запретnull, полезно для ключей.
Вариантность
Вариантность — можно ли использовать Generic<Derived> там, где ожидается Generic<Base>.
- Ковариантность (
out T): тип только отдаёт T (результат, возвращаемое значение).IEnumerable<out T>,IReadOnlyList<out T>,Func<out TResult>.
IEnumerable<Dog> dogs = GetDogs();
IEnumerable<Animal> animals = dogs; // можно: из IEnumerable<Animal> мы только читаем- Контравариантность (
in T): тип только принимает T (параметр).IComparer<in T>,Action<in T>,IEqualityComparer<in T>.
Action<Animal> feedAnimal = a => Feed(a);
Action<Dog> feedDog = feedAnimal; // можно: обработчик для любых животных подойдёт для собак- Инвариантность — по умолчанию:
List<T>,IList<T>,Dictionary<K,V>(изменяемые, T и на вход, и на выход).
List<Animal> a = new List<Dog>(); // ошибка: иначе a.Add(new Cat()) сломало бы список собакОбъявление вариантности своих интерфейсов:
public interface IProducer<out T> { T Produce(); }
public interface IConsumer<in T> { void Consume(T item); }Работает только для интерфейсов и делегатов и только для ссылочных типов в аргументе (IEnumerable<int> в IEnumerable<object> нельзя: потребовался бы boxing).
Массивы ковариантны исторически (object[] o = new string[1]), что даёт исключение ArrayTypeMismatchException при записи неверного типа.
static abstract и generic math (C# 11, .NET 7)
Интерфейс может требовать статические члены и операторы:
public interface IShape<TSelf> where TSelf : IShape<TSelf>
{
static abstract TSelf Create(double size);
static abstract double Area(TSelf shape);
}
public static T Sum<T>(IEnumerable<T> items) where T : INumber<T>
{
T total = T.Zero;
foreach (var x in items) total += x; // оператор + через интерфейс INumber<T>
return total;
}
var s1 = Sum([1, 2, 3]); // int
var s2 = Sum([1.5, 2.5]); // doubleРаньше для этого приходилось дублировать код под каждый числовой тип или использовать dynamic.
Как CLR исполняет generics
- Для каждого значимого типа в аргументе JIT создаёт отдельный нативный код (
List<int>,List<double>): без boxing, максимальная скорость. - Для ссылочных типов код общий (
__Canon): все ссылки одного размера, поэтому один код на все. - Информация о типе сохраняется в рантайме (реификация), в отличие от стирания в Java.
typeof(List<int>) != typeof(List<string>) // разные закрытые типы
typeof(List<>) // открытый generic тип
typeof(List<>).MakeGenericType(typeof(int)) // построение закрытого типа в рантаймеGenerics и DI
services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); // открытый generic: любой IRepository<T>
services.AddTransient(typeof(IValidator<>), typeof(DefaultValidator<>));Полезные приёмы
- CRTP (
where TSelf : Base<TSelf>) — базовый класс знает конкретный тип наследника, например для fluent-билдеров. - Generic-атрибуты (C# 11):
[JsonConverter<T>]вместоtypeof. - Статический generic-класс как кэш:
static class Cache<T> { public static readonly Func<T> Factory = ...; }— у каждого T свои статические поля.
Нюансы и подводные камни
IEnumerable<Dog>вIEnumerable<Animal>— можно,List<Dog>вList<Animal>— нельзя: смешивают ковариантность интерфейса и инвариантность класса.- Вариантность для значимых типов не работает:
IEnumerable<int>не станетIEnumerable<object>. - Сравнение
Tчерез==недоступно без ограничения. ИспользуйтеEqualityComparer<T>.Default. - Открытые generic-регистрации в DI не поддерживают ограничения без явной проверки: при несоответствии получите исключение при разрешении.
- Чрезмерно абстрактные generic-иерархии тяжело читать; generics нужны для реального переиспользования, а не «на всякий случай».
Практика
- Написать
Result<T>с ковариантным интерфейсомIResult<out T>. - Реализовать
SumиAverageдляINumber<T>и сравнить скорость с версией подint. - Объяснить, почему
Action<Dog>нельзя присвоитьAction<Animal>.
Вопросы с ответами
Чем ковариантность отличается от контравариантности?
Ковариантность (
out):Generic<Derived>можно использовать какGeneric<Base>, когда тип только отдаёт значения. Контравариантность (in):Generic<Base>подходит какGeneric<Derived>, когда тип только принимает значения.
Почему
List<Dog>нельзя присвоитьList<Animal>?
List<T>и читает, и записывает T. Иначе через ссылкуList<Animal>в список собак можно было бы добавить кошку, что нарушило бы типобезопасность.
Зачем нужны
static abstractчлены интерфейсов?Чтобы обобщённый код мог вызывать статические члены и операторы типа-аргумента (
T.Zero,a + b). На этом построен generic math (INumber<T>).
Как работают generics в CLR для значимых и ссылочных типов?
Для каждого значимого типа JIT создаёт отдельную версию кода. Для ссылочных используется общий код. Информация о типах сохраняется в рантайме.
Что такое открытый generic-тип и где он нужен?
Тип с неуказанным аргументом (
List<>). Используется в DI (typeof(IRepository<>)), рефлексии и при построении типов в рантайме.
Связанные темы
- Предыдущая: yield, итераторы, IAsyncEnumerable · Следующая: Extension methods и fluent API
- Generics основы: Generics: обобщённые типы и методы
- Интерфейсы: Интерфейсы и абстрактные классы, default interface methods
- DI: 3.2 Dependency Injection
- Generics в TypeScript: Generics