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