Коваріантність і контраваріантність (програмування)

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку

Варіантність — у програмуванні, спосіб перенесення наслідування типів на нові типи параметризовані попередніми (контейнери, узагальнені типи, делегати).

Терміни виникли від термінів теорії категорій «коваріантний» та «контраваріантний» функтор.

Визначення[ред. | ред. код]

В системі типів мови програмування, конструктор типів є:

  • коваріантним — якщо він зберігає порядок типів (від похідних до базового);
  • контраваріантним — якщо він змінює порядок типів на протилежний;
  • біваріантним — якщо прямий і обернений порядок є справедливими одночасно.
  • інваріантним чи неваріантним — якщо він не підпадає під попередні варіанти.

C# приклади[ред. | ред. код]

Наприклад, в C#, якщо Cat це підтип Animal, тоді:

  • IEnumerable<Cat> це підтип IEnumerable<Animal>. Підтипність збережена, бо IEnumerable<T> коваріантний по T.
  • Action<Animal> підтип Action<Cat>. Підтипність обернута, бо Action<T> контраваріантний по T.
  • Ані IList<Cat>, ані IList<Animal> не є підтипом іншого, бо IList<T> інваріантний по T.

Варіантність узагальнених інтерфейсів C# визначається через використання атрибута out (коваріантний) або in (контраваріантний) для (нуля чи більше) його параметрів типів. Для кожного промаркованого таким чином параметра, компілятор перевіряє, не дозволячи жодного порушення, що використання глобально цілісне. Згадані інтерфейси означені як IEnumerable<out T>, Action<in T> і IList<T>. Типи з більш ніж одним параметром типом можуть вказувати різні варіантності для різних параметрів типів. Наприклад, тип делегат Func<in T, out TResult> представляє функцію з контраваріантним входовим параметром типом T і коваріантним параметром типу результату TResult.[1]

Варіантність контейнерів[ред. | ред. код]

Якщо типи Cat та Dog наслідують тип Animal, конструктором масивів з типу Animal утворимо тип Animal[] ("масив звірів"). Ми можемо використовувати його:

Залежно від типу операцій, що будуть проводитись над масивом ми можемо використовувати різну варіантність:

  • якщо тільки читати елементи масиву, то безпечно використовувати його коваріантно, оскільки Cat є представником Animal;
  • якщо тільки записувати чи передавати елементи масиву то безпечно використовувати його контраваріантно, щоб не було можливості в масив Cat[] записати елемент типу Dog;
  • якщо ж потрібно читати та записувати елементи масиву, то безпечно використовувати його тільки інваріантно. Також можна реалізувати два незалежні інтерфейси коваріантний Producer<T> для читання та контраваріантний Consumer<T> для запису елементів.

Масиви є коваріантними в Java, C#, та інваріантними в C++.

Варіантність функційних типів[ред. | ред. код]

Функційні типи є коваріантними по типу результата та контраваріантними по типах аргументів.

Тобто функції типу Cat->Cat та Animal->Animal наслідують тип Cat->Animal та можуть безпечно використовуватись там де очікувався базовий тип.

Варіантність при успадковуванні[ред. | ред. код]

C++ починаючи зі стандарту 1998 року підтримує коваріантність при заміщенні віртуальних методів:

class X {};
class Y : public X {};

class A
{
public:
    virtual X* f() { return new X; }
};

class B : public A
{
public:
    virtual Y* f() { return new Y; } // заміщуючи метод, можна повернути уточнений тип результату
};

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

тип аргумента тип результату
C# інваріантний інваріантний
C++ (з 1998), Java (з J2SE 5.0), Scala, D інваріантний коваріантний
Sather контраваріантний коваріантний
Eiffel коваріантний коваріантний

Варіантність шаблонних типів[ред. | ред. код]

В деяких мовах програмування, що підтримують параметричний поліморфізм, можливо вказати варіантність шаблонного типу по параметру. Це можливо:

  • при декларації шаблонного типу (як в C#) чи
  • при створенні (використанні) конкретного типу на основі шаблонного (як в Java).

Задання варіантності при декларації[ред. | ред. код]

  • в C# це можливо тільки для інтерфейсів (ключовими кловами out та in),
  • в Scala та OCaml (ключовими кловами + та-).

Якщо варіантність не задана, то тип буде вважатись інваріантним.

Приклад
interface IEnumerator<out T>
{
    T Current { get; }
    bool MoveNext();
}

IEnumerator<Cat> буде вважатись успадкованим від IEnumerator<Animal>.

Задання варіантності при використанні[ред. | ред. код]

Задання варіантності при створенні кожного конкретного типу є більш гнучким. Воно дозволяє автору класа не декларувати декілька інтерфейсів з різною варіантністю. В Java варіантність задається разом із заданням обмежуючого типу:

  • List<? extends Animal> буде коваріантним типу елемента;
  • List<? super Animal> буде контраваріантним типу елемента.

При заданні варіантності буде заборонено використовувати методи класу з відмінною варіантністю.

Джерела[ред. | ред. код]

  1. Func<T, TResult> Delegate [Архівовано 19 Серпня 2017 у Wayback Machine.] - MSDN Documentation