C#. Підтримка контраваріантності в узагальнених інтерфейсах

Підтримка контраваріантності в узагальнених інтерфейсах. Ключове слово in

Перед вивченням даної теми рекомендується ознайомитись з наступними темами:


Зміст


Пошук на інших ресурсах:

1. Поняття контраваріантності. Ключове слово in

У мові C#, починаючи з версії 4.0, в узагальненнях було введено коваріантність та контраваріантність. Більш детально про особливості застосування коваріантності для узагальнених типів можна прочитати тут. У даній темі розкривається поняття контраваріантності.

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

Для реалізації механізму контраваріантності необхідні оголошення наступних елементів:

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

Загальна форма оголошення контраваріантного інтерфейсу, який отримує аргументом деякий тип T, має вигляд

interface InterfaceName<in T>
{
  ...
}

тут

  • InterfaceName – ім’я узагальненого інтерфейсу. Модифікатор in вказує на те, що інтерфейс підтримує контраваріантність;
  • T – ім’я типу, який використовується в оголошенні методів інтерфейсу InterfaceName.

Як видно з вищенаведеного коду, для забезпечення контраваріантності перед іменем типу T ставиться ключове слово in.

Клас, що реалізує контраваріантний інтерфейс оголошується стандартним способом

class ClassName<T> : InterfaceName<T>
{
  ...
}

тут

  • ClassName – ім’я узагальненого класу що отримує параметром тип T та реалізує інтерфейс InterfaceName<T>.

Як видно з вищенаведеного фрагменту, модифікатор in перед оголошенням типу T класу не ставиться.

Пара інтерфейс-клас дозволяє реалізувати механізм контраваріантності для узагальненого типу T. В якості типу T можуть використовуватись класи, що утворюють ієрархію. Для двох класів Base та Derived, які утворюють ієрархію

class Base
{
  ...
}

class Derived : Base
{
  ...
}

можна записата наступний код, який забезпечується завдяки контраваріантності

// Оголосити посилання на інтерфейс з типом заповнювачем похідного класу Derived
InterfaceName<Derived> refDerived;

// Створити екземпляр класу ClassName з типом-заповнювачем базового класу Base
ClassName<Base> obj = new ClassName<Base>(...);

// Це присвоєння працює завдяки контраваріантності
refDerived = obj;

Як видно з вищенаведеного коду, модифікатор in в оголошенні інтерфейсу InterfaceName<in T> дозволяє щоб екземпляр класу, який отримує аргументом базовий клас Base, присвоювався посиланню на інтерфейс, який отримує параметром похідний клас Derived.

Іншими словами, якщо з оголошення інтерфейсу прибрати ключове слово in, то в рядку

refDerived = obj;

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

 

2. Вимоги до методів, що оголошуються в контраваріантному інтерфейсі

Методи, що оголошуються в контраваріантному інтерфейсі повинні мати сигнатуру, в якій метод не повертає узагальнений тип T

interface IContraInterface<in T>
{
  ...

  type MethodName(parameters);

  ...
}

тут

  • type – будь-який тип у програмі за винятком типу T. Це може бути один з базових типів (int, float, char тощо) чи будь-який інший тип програми (клас, структура тощо).

 

3. Приклад, що демонструє контраваріантність

У прикладі демонструється механізм контраваріантності для посилань refA та refB.

using System;

namespace ConsoleApp19
{
  // Контраваріантність в узагальненому інтерфейсі.
  // Контраваріантний узагальнений інтерфейс,
  // який отримує параметром тип T.
  interface IContraInterface<in T>
  {
    void Print(); // без параметрів
    void PrintT(T value);
  }

  // Узагальнений клас, який реалізує узагальнений інтерфейс
  class GenClass<T> : IContraInterface<T>
  {
    // Внутрішнє поле класу
    T value;

    // Конструктор
    public GenClass(T _value)
    {
      value = _value;
    }

    // Метод доступу до внутрішнього поля
    public T GetT() { return value; }

    // Реалізація методу PrintT() інтерфейсу.
    // Метод встановлює нове значення в змінній value.
    public void PrintT(T _value)
    {
      Console.WriteLine("{0}", _value);
    }

    public void Print()
    {
      Console.WriteLine(value.ToString());
    }
  }

  // Деяка ієрархія класів A, B.
  // Базовий клас.
  class A
  {
    // Внутрішнє поле класу A
    int a;

    // Конструктор
    public A(int _a) { a = _a; }

    // Метод доступу до поля a
    public int GetA() { return a; }

    // Метод доступу до поля a в рядковому вигляді через клас Object
    public override string ToString()
    {
      return "a = " + Convert.ToString(a);
    }
  }

  // Похідний клас
  class B : A
  {
    // Внутрішнє поле
    int b;

    // Конструктор
    public B(int _a, int _b) : base(_a)
    {
      b = _b;
    }

    // Метод доступу до поля b
    public int GetB() { return b; }

    // Метод доступу до поля b у рядковому вигляді через клас Object
    public override string ToString()
    {
      return "b = " + Convert.ToString(b);
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Демонстрація контраваріантності
      // 1. Створити екземпляри класів A, B
      A objA = new A(10);
      B objB = new B(15, 20);

      // 2. Створити два посилання на контраваріантний інтерфейс,
      //    які отримують параметрами типу класи A та B
      IContraInterface<A> refA;
      IContraInterface<B> refB;

      // 3. Створити екземпляри класу MyContraclass<T>,
      //    в якому параметрами типу виступають класи A, B.
      //    Відповідно, конструктори класу отримують об'єкти objA, objB.
      GenClass<A> objGenClassA = new GenClass<A>(objA);
      GenClass<B> objGenClassB = new GenClass<B>(objB);

      // 4. Демонстрація контраваріантності на прикладі посилань refA, refB
      refA = objGenClassA; // можна у будь-якому випадку
      refA.Print(); // a = 10

      refB = objGenClassB; // можна у будь-якому випадку
      refB.Print(); // b = 20

      // так присвоювати можна завдяки контраваріантності
      refB = objGenClassA; // тут немає строгого дотримання типів
      refB.Print(); // a = 10

      // так не можна, щоб це зробити потрібно реалізувати коваріантність
      // refA = objGenClassB; // тут помилка компіляції

      // Викликати інший метод інтерфейсу PrintT().
      GenClass<A> objGenClassA2 = new GenClass<A>(new A(777));

      // так можна завдяки контраваріантності
      refB = objGenClassA2; // GenClass<B> <= GenClass<A>
      refB.Print(); // a = 777

      // Викликати інший метод інтерфейсу PrintT().
      refB.PrintT(new B(200, 300)); // b = 300

      Console.WriteLine("Ok");
      Console.ReadKey();
    }
  }
}

Результат виконання програми

a = 10
b = 20
a = 10
a = 777
b = 300
Ok

 

4. Рисунок, що пояснює контраваріантність

На рисунку 1 зображено один інтерфейс та мінімальний набір класів, які необхідні для розкриття деталей реалізації контраваріантності. Червоними стрілками виділено відповідні фрагменти коду.

C#. Узагальнення. Контраваріантність в узагальнених інтерфейсах

Рисунок 1. Застосування контраваріантності для ієрархії класів A, B

 

5. Спадковість в контраваріантних інтерфейсах

Контраваріантні інтерфейси можуть успадковувати один одного. Загальний синтаксис успадкування контраваріантних інтерфейсів, що отримують параметром тип T, наступний

interface IBaseInterface<in T>
{
  ...
}

interface IDerivedInterface<in T> : IBaseInterface<T>
{
  ...
}

тут

  • IBaseInterface<in T> – базовий контраваріантний інтерфейс;
  • IDerivedInterface<in T> – успадкований контраваріантний інтерфейс. Якщо в успадкованому інтерфейсі опустити модифікатор in, то цей інтерфейс не буде підтримувати контраваріантність.

 

6. Обмеження, що накладаються на контраваріантність

На контраваріантність накладаються наступні обмеження:

  • контраваріантність може бути застосована тільки для посилальних типів (class);
  • методи, що оголошуються в контраваріантних узагальнених інтерфейсах не повинні повертати узагальнений тип T;
  • методи, що оголошуються в контраваріантних інтерфейсах можуть отримувати тип T в якості параметру.

 


Споріднені теми