Коваріантність в узагальнених інтерфейсах. Ключове слово out
Зміст
- 1. Коваріантність. Особливості застосування для узагальнених інтерфейсів. Ключове слово out
- 2. Приклад, що демонструє роботу механізму коваріантності
- 3. Рисунок, що пояснює коваріантність
- 4. Успадкування коваріантних інтерфейсів
- 5. Обмеження, що накладаються на коваріантність
- Споріднені теми
Пошук на інших ресурсах:
1. Коваріантність. Особливості застосування для узагальнених інтерфейсів. Ключове слово out
Починаючи з версії 4.0 у мову C# було введено розширення, що дозволяє застосовувати коваріантність та контраваріантність для параметру узагальненого типу T в інтерфейсах та делегатах.
Коваріантність дає можливість повертати похідний тип від базового типу, який вказаний в параметрі типу.
Для того, щоб вказати компілятору, що тип T підтримує коваріантність, використовується ключове слово out за наступною формою
out T
Таким чином, загальна форма оголошення узагальненого інтерфейсу, що підтримує коваріантність, наступна
interface ICoVarInterface<out T> { // Оголошення методів, які повертають тип T ... }
тут
- ICoVarInterface – ім’я узагальненого інтерфейсу.
Як видно з вищенаведеного оголошення, тип T в інтерфейсі підтримує коваріантність тому що перед ним вказано ключове слово out.
В інтерфейсі ICoVarInterface<T> потрібно оголошувати тільки методи, які повертають тип T. Загальна форма оголошення такого методу наступна
T MethodName(parameters);
тут
- MethodName – ім’я методу;
- parameters – список параметрів методу. Параметри можуть бути будь-якого типу крім узагальненого типу T.
Після оголошення інтерфейсу можна оголошувати клас. Оголошення класу, який реалізує інтерфейс ICoVarInterface здійснюється звичайним способом
class CoVarClass<T> : ICoVarInterface<T> { ... // Реалізація методів інтерфейсу ICoVarInterface // ... ... }
Вищенаведена пара інтерфейс-клас підтримують коваріантність. Тепер цю пару можна застосовувати до до деякої ієрархії класів, які будуть виступати в якості типу T.
Якщо два класи BaseClass, DerivedClass утворюють ієрархію
class BaseClass { ... } class DerivedClass : BaseClass { ... }
то при оголошенні посилання на узагальнений інтерфейс ICoVarInterface<out T>, в якості параметру типу T завжди вказується тип базового класу
ICoVarInterface<BaseClass> refI;
Створити екземпляр класу CoVarClass<T> можна звичайним способом, вказавши в якості параметру типу базовий клас BaseClass, точніше екземпляр базового класу
BaseClass objBase = new BaseClass(...); // екземпляр базового класу refI = new CoVarClass<BaseClass>(objBase);
Так само, завдяки коваріантності, можна вказувати параметром типу екземпляр похідного класу
DerivedClass objDerived = new DerivedClass(...); // екземпляр похідного класу refI = new CoVarClass<DerivedClass>(objDerived); // працює - забезпечується коваріантність
Якщо в оголошенні інтерфейсу ICoVarInterface забрати ключове слово out
interface ICoVarInterface<T> { ... }
то створити екземпляр класу CoVarClass в якому типом T виступає похідний клас DerivedClass, не вдасться
DerivedClass objDerived = new DerivedClass(...); // екземпляр похідного класу refI = new CoVarClass<DerivedClass>(objDerived); // тут помилка - немає коваріантності
⇑
2. Приклад, що демонструє роботу механізму коваріантності
У прикладі оголошуються:
- узагальнений інтерфейс IMyCoVarInterface<out T>, який отримує параметром тип T. Цей тип T підтримує коваріантність завдяки модифікатору out;
- узагальнений клас MyClass<T>, який реалізує інтерфейс IMyCoVarInterface<out T>. У класі тип T також підтримує коваріантність, оскільки базовий інтерфейс підтримує коваріантність;
- два класи A, B які утворюють ієрархію.
В інтерфейсі IMyCoVarInterface<out T> оголошується єдиний метод GetT(), який повертає посилання типу T.
Клас MyClass<T> містить наступні елементи:
- внутрішнє поле value узагальненого типу T. В подальшому замість типу T буде підставлятись один з екземплярів класів A та B, які утворюють ієрархію;
- конструктор з одним параметром, який ініціалізує значення value;
- метод Get(), який відповідно реалізує метод інтерфейсу.
Для демонстрації коваріантності, у прикладі створено ієрархію з двох класів A та B. У базовому класі A оголошуються наступні складові:
- внутрішнє поле a, що є даними класу;
- конструктор з одним параметром, який ініціалізує поле a;
- віртуальний метод Get() для доступу до внутрішнього поля value.
В успадкованому класі B оголошуються наступні елементи:
- внутрішнє поле b;
- конструктор з двома параметрами. Даний конструктор ініціалізує поле a базового класу та поле b даного класу;
- перевизначений (override) метод Get() для доступу до поля b.
using System; namespace ConsoleApp19 { // Коваріантність в інтерфейсі. // Визначається за ключовим словом out. public interface IMyCoVarInterface<out T> { // Метод, який повертає тип T T GetT(); } // Клас, що реалізує коваріантний інтерфейс class MyClass<T> : IMyCoVarInterface<T> { // Внутрішнє поле T value; // Конструктор public MyClass(T _value) { value = _value; } // Метод, оголошений в інтерфейсі IMyCoVarInterface<T> public T GetT() { return value; } } // Деяка ієрархія класів: A - базовий клас, B - похідний клас. // Базовий клас class A { // Дані класу A int a; // Конструктор public A(int _a) { a = _a; } // Метод доступу public virtual int Get() { return a; } } // Похідний клас class B : A { // Дані класу B int b; // Конструктор public B(int _a, int _b) : base(_a) { b = _b; } // Метод доступу public override int Get() { return b; } } class Program { static void Main(string[] args) { // Демонстрація коваріантності. // 1. Оголосити посилання на інтерфейс, // яке отримує параметром типу ім'я базового класу A IMyCoVarInterface<A> refI; // 2. Створити екземпляр базового класу A A objA = new A(25); // 3. Створити екземпляр класу MyClass, в якому // в якості типу виступає клас A refI = new MyClass<A>(objA); // працює Console.WriteLine("objA.value = " + refI.GetT().Get()); // 4. Створити екземпляр похідного класу B B objB = new B(30, 40); // 5. Присвоїти посиланню refI екземпляр класу MyClass, // в якому типом виступає клас B refI = new MyClass<B>(objB); // працює завдяки коваріантності Console.WriteLine("objB.value = " + refI.GetT().Get()); // 40 Console.ReadKey(); } } }
Пояснимо деякі фрагменти коду. У функції main() створюється два екземпляри objA та objB, відповідно класів A та B
... // 2. Створити екземпляр базового класу A A objA = new A(25); ... // 4. Створити екземпляр похідного класу B B objB = new B(30, 40);
Ці екземпляри підставляються як типи-заповнювачі при створенні екземпляру класу MyClass
... // 3. Створити екземпляр класу MyClass, в якому // в якості типу виступає клас A refI = new MyClass<A>(objA); ... // 5. Присвоїти посиланню refI екземпляр класу MyClass, // в якому типом виступає клас B refI = new MyClass<B>(objB); // працює завдяки коваріантності
Створення екземпляру, в якому типом-заповнювачем є клас A, не потребує пояснень, оскільки посилання refI оголошене для типу A
... // 1. Оголосити посилання на інтерфейс, // яке отримує параметром типу ім'я базового класу A IMyCoVarInterface<A> refI; ...
А щоб створити екземпляр класу B, обов’язково потрібно щоб при оголошенні інтерфейсу для типу T був вказаний модифікатор out, як у нашому випадку
// out - підтримка коваріантності public interface IMyCoVarInterface<out T> { ... }
Якщо прибрати ключове слово out з оголошення інтерфейсу то створити екземпляр класу MyClass<B> не вдасться
// out відсутній - коваріантність не підтримується public interface IMyCoVarInterface<T> { ... } static void Main(string[] args) { ... refI = new MyClass<B>(objB); // помилка компіляції, в інтерфейсі відсутнє слово out ... }
Після запуску на виконання програма видає наступний результат
objA.value = 25 objB.value = 40
⇑
3. Рисунок, що пояснює коваріантність
Наступний рисунок пояснює механізм застосування коваріантності для узагальненого інтерфейсу. Підкреслюється важливість застосування модифікатора out.
Рисунок 1. Коваріантність. Модифікатор out дозволяє створювати екземпляр узагальненого класу, в якому типом виступає похідний клас
⇑
4. Успадкування коваріантних інтерфейсів
Коваріантні інтерфейси можуть буди успадковані. У найбільш загальному випадку успадкування двох інтерфейсів виглядає наступним чином
// Успадкування коваріантних інтерфейсів. // Визначається за ключовим словом out. interface ICoVarInterface<out T> { ... } // Інтерфейс, який успадковує ICoVarInterface<T> interface ICoVarInterface2<out T> : ICoVarInterface<T> { ... }
Як видно з фрагменту, для забезпечення коваріантності в успадкованому інтерфейсі застосовується модифікатор out. Якщо не вказати out, то успадкований інтерфейс ICoVarInterface2 не буде використовувати коваріантність (що не можна сказати про базовий інтерфейс).
При оголошенні успадкованого інтерфейсу ICoVarInterface2<out T>, задавати модифікатор out в базовому інтерфейсі ICoVarInterface<T> не потрібно.
⇑
5. Обмеження, що накладаються на коваріантність
На коваріантність накладаються наступні обмеження.
1. Коваріантність параметру типу T розповсюджується тільки на методи, які повертають тип T. Наприклад, метод з наступною сигнатурою підходить для реалізації коваріантності
T GetItem();
а наступний не підходить
int Method(T value);
оскільки не повертає тип T.
2. Коваріантність параметру типу T не можна застосовувати для методів, які отримують хоча б один параметр типу T.
Наприклад, наступні методи не підходять для реалізації коваріантності
T GetItem(T value); T Calc(int x1, T x2);
оскільки в параметрах методів фігурує тип T.
3. Коваріантність придатна тільки для посилальних типів. Якщо спробувати реалізувати коваріантність для структур, то компілятор згенерує помилку.
4. При оголошенні інтерфейсу коваріантний тип не може бути використаний як обмеження.
Наприклад, оголосити інтерфейс, в якому коваріантний тип T обмежується іншим типом не вдасться
// Коваріантний інтерфейс. // У методі інтерфейсу здійснюється спроба оголосити тип V, // який обмежується типом T interface IMyInterface<out T> { T GetItem<V>() where V : T; // тут помилка компіляції }
⇑
Споріднені теми
- Підтримка контраваріантності в узагальнених інтерфейсах. Ключове слово in
- Ієрархії узагальнених класів. Узагальнений базовий та похідний класи
⇑