Підрозділ 7.7
Коваріантність та контраваріантність узагальнених інтерфейсів
Пояснює коваріантність, контраваріантність і їх поєднання в узагальнених інтерфейсах на прикладах месенджерів.
7.7. Коваріантність та контраваріантність узагальнених інтерфейсів
Уявіть ієрархію: Cardiologist успадковує Doctor. Це означає, що Cardiologist є Doctor: де очікується Doctor, можна підставити Cardiologist. Але чи діє та ж логіка для узагальнених інтерфейсів? Чи можна підставити IClinicFactory<Cardiologist> туди, де очікується IClinicFactory<Doctor>?
Відповідь залежить від того, чи є інтерфейс коваріантним, контраваріантним або інваріантним. Ці три поняття описують, як напрям допустимого присвоєння між параметризованими типами співвідноситься з напрямом ієрархії між типами-аргументами.

Три форми варіантності:
- Коваріантність — присвоєння йде в той самий бік, що й ієрархія класів. Якщо
Cardiologist : Doctor, тоIFactory<Cardiologist>можна присвоїтиIFactory<Doctor>. - Контраваріантність — присвоєння йде в зворотний бік.
IHandler<Doctor>можна присвоїтиIHandler<Cardiologist>. - Інваріантність — присвоєння неможливе в жодному напрямі (поведінка за замовчуванням).
Інваріантність — поведінка за замовчуванням
Без спеціальних ключових слів усі узагальнені інтерфейси є інваріантними: IRepository<Doctor> і IRepository<Cardiologist> — абсолютно несумісні типи, навіть якщо Cardiologist : Doctor. Жодне присвоєння в жодному напрямі не дозволяється.
interface IRepository<T>
{
T GetById(int id);
void Save(T item);
}
// IRepository<Cardiologist> НЕ можна присвоїти IRepository<Doctor> — помилка
// IRepository<Doctor> НЕ можна присвоїти IRepository<Cardiologist> — помилкаЦе може здаватися надто суворим, але є підставовою безпекою: якби IRepository<Cardiologist> можна було підставити як IRepository<Doctor>, то через змінну типу IRepository<Doctor> хтось міг би викликати Save(new Therapist()) — а Therapist не є Cardiologist. Тому без явного дозволу компілятор забороняє такі присвоєння.
Коваріантні інтерфейси (out)
Коваріантний інтерфейс оголошується за допомогою ключового слова out перед параметром типу:
interface IClinicFactory<out T>
{
T Create();
}Ключове слово out означає: «параметр T використовується лише як тип значення, що повертається». Тобто об'єкт типу T виходить з інтерфейсу (звідси out). У такому разі компілятор може гарантувати безпеку: якщо Cardiologist : Doctor, то всі кардіологи, яких повертає IClinicFactory<Cardiologist>, є і лікарями, тому IClinicFactory<Cardiologist> безпечно замінює IClinicFactory<Doctor>.
Обмеження коваріантного інтерфейсу: параметр T з out можна використовувати лише як тип, що повертається — як тип вхідного аргументу методу він заборонений. Це гарантує, що через коваріантний інтерфейс не можна «записати» об'єкт неправильного типу.
Контраваріантні інтерфейси (in)
Контраваріантний інтерфейс оголошується за допомогою ключового слова in:
interface IDoctorHandler<in T>
{
void Handle(T doctor);
}Ключове слово in означає: «параметр T використовується лише як тип вхідного аргументу». Тобто об'єкт типу T входить у інтерфейс. Якщо IDoctorHandler<Doctor> вміє обробляти будь-якого лікаря, то він точно вміє обробляти і кардіолога (бо кардіолог є лікарем). Тому IDoctorHandler<Doctor> можна присвоїти IDoctorHandler<Cardiologist> — зворотний напрям відносно ієрархії.
Обмеження контраваріантного інтерфейсу: параметр T з in можна використовувати лише як тип вхідного аргументу — як тип значення, що повертається, він заборонений. Це гарантує, що через контраваріантний інтерфейс не можна «прочитати» об'єкт неправильного типу.
Поєднання коваріантності та контраваріантності
Один і той самий інтерфейс може мати кілька параметрів типу, одні з яких є out, а інші — in. Наприклад, інтерфейс клінічного месенджера, що приймає повідомлення одного типу і повертає відповідь іншого:
Завдяки обом модифікаторам об'єкт ClinicMessengerImpl (тип IClinicMessenger<Doctor, DetailedReport>) можна безпечно використовувати як IClinicMessenger<Cardiologist, Report> — контраваріантність по T (Doctor → Cardiologist, зворотний напрям) і коваріантність по K (DetailedReport → Report, прямий напрям).
Правила і обмеження
| Модифікатор | Назва | Дозволено для T | Напрям присвоєння |
|---|---|---|---|
out T |
Коваріантний | Лише тип результату методу | IFoo<Derived> → IFoo<Base> |
in T |
Контраваріантний | Лише тип аргументу методу | IFoo<Base> → IFoo<Derived> |
| (без модифікатора) | Інваріантний | Будь-яке використання | Присвоєння заборонено |
Ці модифікатори доступні лише для інтерфейсів і делегатів — не для класів і структур. Саме тому в стандартній бібліотеці .NET такі інтерфейси, як IEnumerable<out T> (коваріантний) і IComparer<in T> (контраваріантний), ведуть себе інтуїтивно: послідовність кардіологів є послідовністю лікарів, а компаратор лікарів може порівнювати кардіологів.