Полное руководство по языку программирования С# 14 и платформе .NET 10 / Делегаты, события и лямбды

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

Начальный ⏱ 4 мин чтения 👁 2 🕓 обновлено 11.08.2026

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

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


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.

Дополнительные материалы

Проверь себя

📋 Тест по теме «Ковариантность и контравариантность делегатов» Тест по теме

Практические задания

Лёгкое Ручная проверка Заполните пропуск в коде (1)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

class _____
{
    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}");
}
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (2)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

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

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

delegate Message MessageBuilder(string text);
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (3)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

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

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

delegate void EmailReceiver(EmailMessage message);
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (4)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

// возвращает EmailMessage - более конкретный тип
MessageBuilder<EmailMessage> _____ = (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);
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (5)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

// принимает объект более общего типа
MessageReceiver<Message> _____Receiver = (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);
Войдите, чтобы отправить решение
Среднее Ручная проверка Восстановите код по памяти (1)
+20 XP

В этом фрагменте кода урока пропущены два ключевых элемента. Впишите оба пропущенных значения через запятую в порядке появления (например: x, 5).

class _____
{
    public string Text { get; }
    public Message(string _____) => 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}");
}
Войдите, чтобы отправить решение
Среднее Ручная проверка Восстановите код по памяти (2)
+20 XP

В этом фрагменте кода урока пропущены два ключевых элемента. Впишите оба пропущенных значения через запятую в порядке появления (например: x, 5).

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

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

delegate Message MessageBuilder(string text);
Войдите, чтобы отправить решение
Среднее Ручная проверка Расставьте строки кода в правильном порядке
+20 XP

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

EmailMessage WriteEmailMessage(string text) => new EmailMessage(text);
delegate Message MessageBuilder(string text);
message.Print();    // Email: Hello
// делегату с базовым типом передаем метод с производным типом
MessageBuilder messageBuilder = WriteEmailMessage; // ковариантность
Message message = messageBuilder("Hello");
Войдите, чтобы отправить решение
Сложное Ручная проверка Объясните и модифицируйте пример из урока «Ковариантность и контравариантность делегатов»
+30 XP

Опишите своими словами, что делает приведённый пример кода из урока «Ковариантность и контравариантность делегатов», и предложите одно осмысленное изменение (например, другое входное значение или дополнительное условие). Приведите итоговый код.

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}");
}
Войдите, чтобы отправить решение
Сложное Ручная проверка Найдите потенциальную проблему в коде урока «Ковариантность и контравариантность делегатов»
+30 XP

Изучите приведённый фрагмент кода и опишите, при каких входных данных или условиях он может сработать не так, как ожидается (граничный случай, отсутствие проверки и т.п.). Предложите исправленный вариант.

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

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

delegate Message MessageBuilder(string text);
Войдите, чтобы отправить решение
Мини-проект Ручная проверка Мини-проект по теме «Ковариантность и контравариантность делегатов»
+50 XP

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

Войдите, чтобы отправить решение
Домашнее задание Ручная проверка Домашнее задание по теме «Ковариантность и контравариантность делегатов»
+25 XP

Напишите собственную небольшую программу, применяющую материал темы «Ковариантность и контравариантность делегатов». Используйте не менее одной конструкции, показанной в примерах урока. Прикрепите код решения и краткое описание того, что он делает.

Войдите, чтобы отправить решение

Обсуждение (0)

Войдите, чтобы оставить комментарий.