Понятия ковариантности и контравариантности связаны с возможностью использовать в приложении вместо некоторого типа другой тип, который находится ниже или выше в иерархии наследования.
Имеется три возможных варианта поведения:
Ковариантность: позволяет использовать более конкретный тип, чем заданный изначально
Контравариантность: позволяет использовать более универсальный тип, чем заданный изначально
Инвариантность: позволяет использовать только заданный тип
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);