Ковариантность и контравариантность обобщенных интерфейсов

Последнее обновление: 02.01.2022

Понятия ковариантности и контравариантности связаны с возможностью использовать в приложении вместо некоторого типа другой тип, который находится ниже или выше в иерархии наследования.

Имеется три возможных варианта поведения:

  • Ковариантность: позволяет использовать более конкретный тип, чем заданный изначально

  • Контравариантность: позволяет использовать более универсальный тип, чем заданный изначально

  • Инвариантность: позволяет использовать только заданный тип

C# позволяет создавать ковариантные и контравариантные обобщенные интерфейсы. Эта функциональность повышает гибкость при использовании обобщенных интерфейсов в программе. По умолчанию все обобщенные интерфейсы являются инвариантными.

Для рассмотрения ковариантных и контравариантных интерфейсов возьмем следующие классы:

class Message
{
    public string Text { get; set; }
    public Message(string text) => Text = text;
}
class EmailMessage : Message
{
    public EmailMessage(string text): base(text) { }
}

Здесь определен класс сообщения Message, который получает через конструктор текст и сохраняет его в свойство Text. А класс EmailMessage представляет условное email-сообщение и просто вызывает конструктор базового класса, передавая ему текст сообщения.

Ковариантные интерфейсы

Обобщенные интерфейсы могут быть ковариантными, если к универсальному параметру применяется ключевое слово out. Такой параметр должен представлять тип объекта, который возвращается из метода. Например:

interface IMessenger<out T>
{
    T WriteMessage(string text);
}
class EmailMessenger : IMessenger<EmailMessage>
{
    public EmailMessage WriteMessage(string text)
    {
        return new EmailMessage($"Email: {text}");
    }
}

Здесь обобщенный интерфейс IMessenger представляет интерфейс мессенджера и определяет метод WriteMessage() для создания сообщения. При этом на момент определения интерфейса мы не знаем, объект какого типа будет возвращаться в этом методе. Ключевое слово out в определении интерфейса указывает, что данный интерфейс будет ковариантным.

Класс EmailMessenger, который представляет условную программу для отправки email-сообщений, реализует этот интерфейс и возвращает из метода WriteMessage() объект EmailMessage.

Применим данные типы в программе:

IMessenger<Message> outlook = new EmailMessenger();
Message message = outlook.WriteMessage("Hello World");
Console.WriteLine(message.Text);    // Email: Hello World


IMessenger<EmailMessage> emailClient = new EmailMessenger();
IMessenger<Message> messenger = emailClient;
Message emailMessage = messenger.WriteMessage("Hi!");
Console.WriteLine(emailMessage.Text);    // Email: Hi!

То есть мы можем присвоить более общему типу IMessenger<Message> объект более конкретного типа EmailMessenger или IMessenger<EmailMessage>.

В то же время если бы мы не использовали ключевое слово out:

interface IMessenger<T>

то мы столкнулись бы с ошибкой в строке

IMessenger<Message> outlook = new EmailMessenger();  // ! Ошибка

IMessenger<EmailMessage> emailClient = new EmailMessenger();
IMessenger<Message> messenger = emailClient;	// ! Ошибка

Поскольку в этом случае невозможно было бы привести объект IMessenger<EmailMessage> к типу IMessenger<Message>

При создании ковариантного интерфейса надо учитывать, что универсальный параметр может использоваться только в качестве типа значения, возвращаемого методами интерфейса. Но не может использоваться в качестве типа аргументов метода или ограничения методов интерфейса.

Контравариантные интерфейсы

Для создания контравариантного интерфейса надо использовать ключевое слово in. Например, возьмем те же классы Message и EmailMessage и определим следующие типы:

interface IMessenger<in T>
{
    void SendMessage(T message);
}
class SimpleMessenger : IMessenger<Message>
{
    public void SendMessage(Message message)
    {
        Console.WriteLine($"Отправляется сообщение: {message.Text}");
    }
}

Здесь опять же интерфейс IMessenger представляет интерфейс мессенджера и определяет метод SendMessage() для отправки условного сообщения. Ключевое слово in в определении интерфейса указывает, что этот интерфейс - контравариантный.

Класс SimpleMessenger представляет условную программу отправки сообщений и реализует этот интерфейс. Причем в качестве типа используемого этот класс использует тип Message. То есть SimpleMessenger фактически представляет тип IMessenger<Message>.

Применим эти типы в программе:

IMessenger<EmailMessage> outlook = new SimpleMessenger();
outlook.SendMessage(new EmailMessage("Hi!"));

IMessenger<Message> telegram = new SimpleMessenger();
IMessenger<EmailMessage> emailClient = telegram;
emailClient.SendMessage(new EmailMessage("Hello"));

Так как интерфейс IMessenger использует универсальный параметр с ключевым словом in, то он является контравариантным, поэтому в коде мы можем переменной типа IMessenger<EmailMessage> передать объект IMessenger<Message> или SimpleMessenger

Если бы ключевое слово in не использовалось бы, то мы не смогли бы это сделать. То есть объект интерфейса с более универсальным типом приводится к объекту интерфейса с более конкретным типом.

При создании контрвариантного интерфейса надо учитывать, что универсальный параметр контрвариантного типа может применяться только к аргументам метода, но не может применяться к возвращаемому результату метода.

Совмещение ковариантности и контравариантности

Также мы можем совместить ковариантность и контравариантность в одном интерфейсе. Например:

interface IMessenger<in T, out K>
{
    void SendMessage(T message);
    K WriteMessage(string text);
}
class SimpleMessenger : IMessenger<Message, EmailMessage>
{
    public void SendMessage(Message message)
    {
        Console.WriteLine($"Отправляется сообщение: {message.Text}");
    }
    public EmailMessage WriteMessage(string text)
    {
        return new EmailMessage($"Email: {text}");
    }
}

Фактически здесь объединены два предыдущих примера. Благодаря ковариантности/контравариантности объект класса SimpleMessenger может представлять типы IMessenger<EmailMessage, Message>, IMessenger<Message, EmailMessage>, IMessenger<Message, Message> и IMessenger<EmailMessage, EmailMessage>. Применение классов:

IMessenger<EmailMessage, Message> messenger = new SimpleMessenger();
Message message = messenger.WriteMessage("Hello World");
Console.WriteLine(message.Text);
messenger.SendMessage(new EmailMessage("Test"));

IMessenger<EmailMessage, EmailMessage> outlook = new SimpleMessenger();
EmailMessage emailMessage = outlook.WriteMessage("Message from Outlook");
outlook.SendMessage(emailMessage);

IMessenger<Message, Message> telegram = new SimpleMessenger();
Message simpleMessage = telegram.WriteMessage("Message from Telegram");
telegram.SendMessage(simpleMessage);
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850