C#. Ковариантность в обобщенных интерфейсах. Ключевое слово out

Ковариантность в обобщенных интерфейсах. Ключевое слово out


Содержание


Поиск на других ресурсах:

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(...); // derived class instance
refI = new CoVarClass<DerivedClass>(objDerived); // here is an error - there is no covariance

 

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.

C#. Обобщения. Ковариантность. Модификатор 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; // здесь ошибка компиляции
}

 


Связанные темы