Перейти к основному содержимому

5.05. Ковариантность, контравариантность, инвариантность

Разработчику Архитектору

Ковариантность, контравариантность, инвариантность

Ковариантность, контравариантность, инвариантность.

Это продвинутые концепции, связанные с подстановкой типов в обобщённых интерфейсах и делегатах. Начнём с примера.

У нас есть проблема - можно ли присвоить List<string> переменной типа List<object>?

List<string> strings = new List<string>();
// List<object> objects = strings; // ❌ Ошибка! Несмотря на то, что string → object

Почему? Потому что List<T> — инвариантный. Лучше использовать IEnumerable<T>.

Вариантность — это способность обобщённого типа «сохранять» или «инвертировать» отношения наследования при подстановке типов.

Допустим:

string → object  // string наследует object

Теперь вопрос: если T → U, то как связаны IEnumerable<T> и IEnumerable<U>?

Ответ зависит от вариантности.

  1. Ковариантность (out) — сохраняет направление наследования.
T → U  =>  I<T> → I<U>

Используется с out — тип используется только для возврата (выход).

Пример: IEnumerable<out T>

IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // ✅ Работает!

Почему можно? Потому что IEnumerable<T> только возвращает элементы — безопасно.

Нельзя добавлять в ковариантный тип — только читать.

  1. Контравариантность (in) — инвертирует направление наследования.
T → U  =>  I<U> → I<T>

Используется с in — тип используется только для входа (параметры).

IComparer<object> comparer = new MyObjectComparer();
IComparer<string> stringComparer = comparer; // ✅ Работает!

Почему? Потому что IComparer<string> может использовать IComparer<object> — любой string можно сравнить как object.

Нельзя возвращать такой тип — только принимать.

  1. Инвариантность — нет подстановки. Даже если T → U, C<T> и C<U> — несовместимы. Пример:
List<T>, Dictionary<TKey, TValue>
List<string> strings = new List<string>();
// List<object> objects = strings; // ❌ Ошибка

Почему? Потому что можно добавить new object() в List<object>, но это сломает List<string>.

Подытожим:

ВариантностьКлючевое словоНаправлениеГде используется
КовариантностьoutТолько возвращаемые значения (например, T Get())IEnumerable<out T>, Func<out TResult>
КонтрвариантностьinТолько параметры (например, void Set(T value))IComparer<in T>, Action<in T>
ИнвариантностьНет измененийList<T>, Dictionary<TKey, TValue>

Обобщённые делегаты

Делегаты тоже могут быть обобщёнными. Это мощно для абстракции и обратных вызовов.

Action<T>       // void Method(T param)
Func<T, TResult> // TResult Method(T param)
Predicate<T> // bool Method(T param)

Делегаты имеют несколько применений. Некоторые из них — механизм обратного вызова, многоадресная рассылка, асинхронная обработка, а также методы абстрагирования и инкапсуляции.

Func<in T, out TResult> — контравариантен по T, ковариантен по TResult. Это позволяет, например, использовать Func<object, string> там, где ожидается Func<string, string>.

Используются обобщения при работе с коллекциями (List<T>, HashSet<T>), при повторяющемся коде для разных типов (Box<T>, Result<T>), с алгоритмами, независимыми от типа (Max<T>, Sort<T>), при создании библиотек (Repository<T>, Serializer<T>), и в интерфейсах и абстракции (IRepository<T>, IService<T>).