Коваріація та контрваріація 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# зводиться до двох тверджень:
- З методу ми можемо повертати похідні типи (спадкоємців) від заявленої сигнатури методу – і це буде коваріація.
- При виклик методу, ми можемо передавати в нього похідні типи (спадкоємців) від тих, що заявлені сигнатурою методу - і це буде контрваріація.
Обидва ці випадки використовують цілком звичне приведення похідного класу до батьківського.
Коваріація та контрваріація у делегатах
Приклади вище дещо умоглядні, давайте спробуємо коваріацію та контрваріацію, використовуючи делегати.
Код прикладу з підступністю:
У змінну з типом делегата 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# підтримується для узагальнених делегатів та інтерфейсів, при цьому тип узагальнення може бути лише посилальним типом.
Коваріація дозволяє наводити похідний тип узагальнення до базового і працює тільки для типів методів, що використовуються як вихідне значення, завдяки тому, що повертати ми можемо тип похідний від того, що потрібно і він буде приведений до необхідного батьківського типу.
Контрваріація дозволяє наводити базовий тип узагальнення до похідного і працює тільки для типів методів, що використовуються як параметри, завдяки тому, що передати в метод ми можемо тип похідний від того, що потрібно і він буде приведений до необхідного батьківського.типу.