Підрозділ 6.4
Коваріантність та контраваріантність делегатів
Пояснює коваріантність і контраваріантність делегатів, їх роботу з похідними й базовими типами та використання in/out в узагальнених делегатах.
6.4. Коваріантність та контраваріантність делегатів
Коваріантність і контраваріантність описують, як делегати взаємодіють зі спадкуванням типів. Вони дозволяють присвоювати делегату метод, сигнатура якого не збігається з делегатом точно, але є сумісною через ієрархію класів. Це підвищує гнучкість коду: замість того щоб оголошувати окремий делегат для кожного типу ієрархії, можна використати один делегат для роботи з базовим або похідним типом.
- Коваріантність стосується типу, що повертається: метод може повертати більш похідний (конкретніший) тип, ніж оголошено в делегаті.
- Контраваріантність стосується параметрів: метод може приймати більш загальний (базовий) тип, ніж оголошено в делегаті.
Розглянемо ці поняття на прикладі ієрархії класів медичних сповіщень:
Клас Notification — базовий для всіх типів сповіщень. EmailNotification і SmsNotification — похідні класи, кожен з яких перевизначає метод Print.
Коваріантність
Коваріантність дозволяє передати делегату метод, тип якого, що повертається, є похідним від типу, що повертається делегатом. Якщо делегат оголошує повернення Notification, то метод може повертати EmailNotification:
Компілятор дозволяє це, бо будь-який об'єкт EmailNotification є водночас об'єктом Notification — відносини «є» (is-a). Якщо делегат очікує Notification, метод, що повертає EmailNotification, завжди задовольняє цю вимогу.
Контраваріантність
Контраваріантність дозволяє передати делегату метод, тип параметра якого є базовим по відношенню до типу параметра делегата. Якщо делегат оголошує параметр EmailNotification, то метод може приймати Notification:
На перший погляд це може здатися суперечливим: делегат оголошує EmailNotification, а метод приймає Notification. Але логіка правильна: при виклику receiver(...) ми завжди передаємо EmailNotification. Будь-який EmailNotification є Notification, тому метод ProcessNotification(Notification n) коректно обробить його. Метод з ширшим типом параметра — більш гнучкий, і все, що він може зробити з Notification, він зможе зробити і з EmailNotification.

Коваріантність та контраваріантність в узагальнених делегатах
Узагальнені делегати також підтримують коваріантність і контраваріантність — через ключові слова out і in у параметрах типу.
Коваріантний узагальнений делегат (out)
Ключове слово out у параметрі типу означає, що цей тип використовується лише як тип, що повертається. Завдяки цьому делегат із більш конкретним типом можна присвоїти змінній делегата з більш загальним типом:
Без out компілятор заборонив би таке присвоєння, навіть попри те, що EmailNotification є Notification.
Контраваріантний узагальнений делегат (in)
Ключове слово in означає, що параметр типу використовується лише як тип параметра делегата. Завдяки цьому делегат із більш загальним типом можна присвоїти змінній делегата з більш конкретним типом:
Як і у випадку з узагальненими інтерфейсами: параметр коваріантного типу (out) застосовується лише до типу, що повертається, а параметр контраваріантного типу (in) — лише до параметрів делегата.
Поєднання коваріантності та контраваріантності
Один узагальнений делегат може одночасно використовувати обидва оператори — in для параметра і out для типу, що повертається:
Тут делегат converter очікує: взяти SmsNotification і повернути Notification. Ми присвоїли йому toEmail, який бере будь-який Notification і повертає EmailNotification. Контраваріантність по M означає: SmsNotification → Notification (параметр стає ширшим). Коваріантність по E означає: EmailNotification → Notification (тип повернення стає ширшим). Обидві заміни безпечні, і компілятор це перевіряє статично.
Якщо узагальнити: коваріантність — від більш похідного до більш загального типу (EmailNotification → Notification), контраваріантність — від більш загального до більш похідного (Notification → EmailNotification).
