Ковариантность и контравариантность делегатов

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

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

Рассмотрим ковариантность и контравариантность на примере следующих классов:

class Message
{
    public string Text { get; }
    public Message(string text) => Text = text;
    public virtual void Print() => Console.WriteLine($"Message: {Text}");
}
class EmailMessage: Message
{
    public EmailMessage(string text): base(text) { }
    public override void Print() => Console.WriteLine($"Email: {Text}");
}
class SmsMessage : Message
{
    public SmsMessage(string text) : base(text) { }
    public override void Print() => Console.WriteLine($"Sms: {Text}");
}

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

Ковариантность

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

// делегату с базовым типом передаем метод с производным типом
MessageBuilder messageBuilder = WriteEmailMessage; // ковариантность
Message message = messageBuilder("Hello");
message.Print();    // Email: Hello

EmailMessage WriteEmailMessage(string text) => new EmailMessage(text);

delegate Message MessageBuilder(string text);

Здесь делегат MessageBuilder возвращает объект Message. Однако благодаря ковариантности данный делегат может указывать на метод, который возвращает объект производного типа, например, на метод WriteEmailMessage.

Контрвариантность

Контрвариантность позволяет присваить делегату метод, тип параметра которого является более универсальным по отношению к типу параметра делегата. Например, возьмем выше определенные классы Message и EmailMessage и используем их в следующем примере:

// делегату с производным типом передаем метод с базовым типом
EmailReceiver emailBox = ReceiveMessage; // контравариантность
emailBox(new EmailMessage("Welcome"));  // Email: Welcome

void ReceiveMessage(Message message) => message.Print();

delegate void EmailReceiver(EmailMessage message);

Несмотря на то, что делегат в качестве параметра принимает объект EmailMessage, ему можно присвоить метод, у которого параметр представляет базовый тип Message. Может показаться на первый взгляд, что здесь есть некоторое противоречие, то есть использование более универсального тип вместо более производного. Однако в реальности в делегат при его вызове мы все равно можем передать только объекты типа EmailMessage, а любой объект типа EmailMessage является объектом типа Message, который используется в методе.

Ковариантность и контравариантность в обобщенных делегатах

Обобщенные делегаты также могут быть ковариантными и контравариантными, что дает нам больше гибкости в их использовании.

Ковариантность

Например, объявим и используем ковариантный обобщенный делегат:

// возвращает EmailMessage - более конкретный тип
MessageBuilder<EmailMessage> EmailMessageWriter = (string text) => new EmailMessage(text);
// возвращает более общий тип Message
MessageBuilder<Message> messageBuilder = EmailMessageWriter;     // ковариантность
Message message = messageBuilder("hello Tom"); // вызов делегата
message.Print(); // Email: hello Tom

delegate T MessageBuilder<out T>(string text);

Благодаря использованию out мы можем присвоить делегату типа MessageBuilder<Message> (более общий тип) делегат типа MessageBuilder<EmailMessage> (более конкретный тип).

Контравариантность

Рассмотрим контравариантный обобщенный делегат:

// принимает объект более общего типа
MessageReceiver<Message> messageReceiver = (Message message) => message.Print();
// принимает объект более конкретного типа
MessageReceiver<EmailMessage> emailMessageReceiver = messageReceiver; // контравариантность

messageReceiver(new Message("Hello World!"));       // Message: Hello World!
messageReceiver(new EmailMessage("Hello World!"));  // Email: Hello World!

delegate void MessageReceiver<in T>(T message);

Использование ключевого слова in позволяет присвоить делегату с производным типом (MessageReceiver<EmailMessage>) делегат с базовым типом (MessageReceiver<Message>).

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

То есть, если грубо обобщить, ковариантность - это от более производного к более общему типу (EmailMessage -> Message), а контрвариантность - от более общего к более производному типу (Message -> EmailMessage).

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

Причем делегат может одновременно использовать оба оператора: in и out. Например:

MessageConverter<Message, EmailMessage> toEmailConverter = (Message message) => new EmailMessage(message.Text);

MessageConverter<SmsMessage, Message> converter = toEmailConverter;
Message message = converter(new SmsMessage("Hello work"));
message.Print();    // Email: Hello work

delegate E MessageConverter<in M, out E>(M message);

Здесь делегат MessageConverter представляет условное действие, которое конвертирует объект типа M в тип E.

В программе определена переменная converter, которая представляет тип MessageConverter<SmsMessage, Message> - то есть конвертер из типа SmsMessage в любой тип Message, грубо говоря преобразует смс в любой другой тип сообщения.

Этой переменной можно передать действие - toEmailConverter, которое из сообщений любого типа создает объект Email-сообщения. Здесь применяется контравариантность: для параметра вместо производного типа SmsMessage применяется базовый тип Message. И также есть ковариантность: вместо возвращаемого типа Message используется производный тип EmailMessage.

Дополнительные материалы
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850