Коваріація та контрваріація C# – Бояринцев

Веб-розробка на .NET

Коваріація та контрваріація C#

Рекомендую паблік у вконтакті - Пасіонарний Дотнетчик, в якому відстежуються та публікуються новини та свіжі статті про дотнет. Також вони мають телеграм-канал.

Що таке коваріація та контрваріація

Припустимо ми маємо два типи - тип Animal і тип Cat і Cat успадковується від Animal - тобто ми можемо Cat привести до Animal і є третій тип, який використовує типи Cat і Animal, допустимо це тип Zoo. Тип Zoo є підступним, якщо ми можемо привести Zoo і є контрваріантним, якщо ми можемо привести до Zoo . Я використовував синтаксис узагальнення у прикладі, але взагалі – це буде не обов'язково узагальнення – сенс у тому, що один тип використовує інші типи та підтримує приведення цих типів у контексті себе.

Класичним є приклад із наведенням списку:

string - успадковується від object та колекцію string ми можемо привести до колекції object.

Тобто, якщо певний метод, як прийнятий параметр використовує IEnumerable , то ми можемо передати в цей метод змінну з типом IEnumerable і вона буде приведена до потрібного типу.

Сенс коваріації та контрваріації у C#

Коваріація та контрваріація в C# існує тільки в контексті методів. Давайте уявимо, що під типом Zoo з прикладу вище ми маємо на увазі метод, а типи Cat і Animal - це типи значення, що повертається або вхідного параметра - тобто під Zoo (де SomeType - Cat або Animal) ми маємо на увазі:

Тепер давайте уявимо, що ми маємо наступний метод:

Сигнатурою методу визначено тип Animal, що повертається, а ми повертаємо Cat, ось як цей метод буде використовуватися:

Цей код повністю працездатний - ми можемо повертати зметоду, тип похідний від заявленого сигнатурою методу і буде приведено до базового.

Ми, фактично, могли б використовувати метод із сигнатурою:

І не помітили б цього у зухвалому коді:

Це і є коваріація - у нас є метод Zoo, який використовує як повертається значення тип Animal, але замість нього, ми можемо використовувати метод з значенням типу Cat, що повертається. Можна сказати, що сигнатура методу:

наводиться до сигнатури:

або можна записати так:

І ви можете запам'ятати перше правило:Коваріація дозволена для методу, що повертається.

Тепер, інший приклад, уявіть сигнатуру методу:

І код його виклику:

Даний код теж повністю працездатний - ми можемо передати як параметр похідний тип і він безболісно буде приведений до базового і метод нормально відпрацює.

Ми можемо замість сигнатури:

Це – контрваріація. У нас є метод Zoo, який як параметр використовує тип Animal і ми можемо використовувати його замість методу Zoo, який як параметр приймає Cat, тобто можна сказати, що сигнатура методу:

наводиться до сигнатури:

або це можна записати так:

І запам'ятати друге правило:Контрваріація дозволена для параметрів методу

Весь зміст коваріації та контрваріації в C# зводиться до двох тверджень:

  1. З методу ми можемо повертати похідні типи (спадкоємців) від заявленої сигнатури методу – і це буде коваріація.
  2. При виклик методу, ми можемо передавати в нього похідні типи (спадкоємців) від тих, що заявлені сигнатурою методу - і це буде контрваріація.

Обидва ці випадки використовують цілком звичне приведення похідного класу до батьківського.

Коваріація та контрваріація у делегатах

Приклади вище дещо умоглядні, давайте спробуємо коваріацію та контрваріацію, використовуючи делегати.

Код прикладу з підступністю:

У змінну з типом делегата delegate Animal delegateAnimal(); - значення, що повертається Animal

Ми поміщаємо метод із сигнатурою:

Повертається значення Cat.

Приклад із контрваріацією використовуючи делегат, пропоную зробити самостійно як домашню роботу.

Логічним кроком далі була б можливість привласнити змінну з типом delegate Animal delegateAnimal(); змінну із типом delegate Cat delegateCat(); але C# не підтримує коваріацію та контрваріацію використовуючи змінні з типами звичайних делегатів, зате підтримує використовуючи узагальнені делегати.

Коваріація та контрваріація в узагальнених делегатах

Розглянемо узагальнений делегат Func - цей делегат використовується для методів із сигнатурою:

Перепишемо приклад вище, використовуючи цей делегат:

Це ключове слово, говорить компілятору, що тип T буде використовуватися тільки як значення методу, що повертається, а це означає для нього дозволена коваріація (замість типу повертається значення ми можемо повернути похідний тип).

Тепер розглянемо інший стандартний узагальнений делегат: Action

Його сигнатура: delegate Action (T param)

Він використовується для методів без значення, що повертається, і одним параметром:

Оскільки в параметри методу ми можемо передавати тип похідний від необхідного, цей делегат має підтримувати контрваріацію.

Ми можемо як присвоїти делегату з типом Action метод з типом прийнятого параметра Animal, так і можемо змінною з типом Action присвоїти змінну з типом Action - тобто привести Action до Action - щоє контрваріацією.

Воно говорить компілятору, що цей тип використовуватиметься лише як параметр методу - отже замість цього ми можемо використовувати похідний.

Приклад використання:

Коваріація в узагальнених делегатах дозволяє нам замість делегата Func використовувати делегат Func .

Контрваріація, замість узагальненого делегата Action дозволяє використовувати делегат Action.

При написанні своїх узагальнених делегатів, ви можете використовувати ключові слова in і out у типів узагальнення - і це автоматично включить ковариацию і контрваріацію цих делегатів.

Самостійна робота:

А що щодо делегата Func? - як для нього працюватиме коваріація та контрваріація?

Коваріація та контрваріація в узагальнених інтерфейсах

Коваріацію узагальненого інтерфейсу, ми вже бачили - на самому початку, коли змінною з типом IEnumerable ми привласнили змінну з типом IEnumerable, працює вона так само як і в узагальнених делегатах завдяки використанню ключового слова у типі узагальнення:

І означає, що даний тип T в цьому інтерфейсі буде використовуватися тільки як значення методів, що повертається, давайте вивчимо цей інтерфейс:

дивимося інтерфейс IEnumerator:

T - це значення значення Current, що повертається (властивості фактично реалізуються як методи, властивість тільки з get - ідентично методу, який не має вхідних параметрів, а тільки значення, що повертається), справжній тип значення, що повертається буде string , але воно буде приводитися до типу Object .

Приклад контрваріації в стандартних узагальнених інтерфейсах знайти складніше, тому напишемо його самі:

Змінною з типом IZoo ми можемо присвоїти змінну з типом IZoo :

При викликуметоду PutZoo(new Cat()) буде викликаний метод PutZoo() класу ZooAnimal, який приймає параметр з типом Animal - ми ж передаємо Cat, який може бути приведений до типу Animal, оскільки є його спадкоємцем.

Обмеження

Коваріація та контрваріація не дозволена для узагальнених класів

Компілятор не дозволить вам визначити тип узагальнення у класу, використовуючи ключові слова in/out і не підтримуватиме коваріацію та контрваріацію для цього класу.

Коваріація та контрваріація працює тільки для типів посилань

Взагалі успадкування структур - не підтримується мовою, тому зробити якусь ієрархію структур, які можна було б приводити один до одного не вийде за всього бажання. Але мова підтримує таку можливість як упаковка, коли значний тип ми можемо запакувати в тип object :

Однак робити приведення типу:

Коваріація в масивах

Коваріація в масивах - це поведінка, що дісталася у спадок.

Масив із типом Cat[] ми можемо призвести до масиву з типом Animal[]

Коваріація та контрваріація в C# підтримується для узагальнених делегатів та інтерфейсів, при цьому тип узагальнення може бути лише посилальним типом.

Коваріація дозволяє наводити похідний тип узагальнення до базового і працює тільки для типів методів, що використовуються як вихідне значення, завдяки тому, що повертати ми можемо тип похідний від того, що потрібно і він буде приведений до необхідного батьківського типу.

Контрваріація дозволяє наводити базовий тип узагальнення до похідного і працює тільки для типів методів, що використовуються як параметри, завдяки тому, що передати в метод ми можемо тип похідний від того, що потрібно і він буде приведений до необхідного батьківського.типу.