Ограничения обобщений
С помощью универсальных параметров мы можем типизировать обобщенные классы любым типом. Однако иногда возникает необходимость конкретизировать тип. Например, у нас есть следующий класс Message, который представляет некоторое сообщение:
class Message
{
public string Text { get; } // текст сообщения
public Message(string text)
{
Text = text;
}
}
И, допустим, мы хотим определить метод для отправки сообщений в виде объектов Message. На первый взгляд мы можем определить и использовать следующий метод:
SendMessage(new Message("Hello World"));
void SendMessage(Message message)
{
Console.WriteLine($"Отправляется сообщение: {message.Text}");
}
Метод SendMessage в качестве параметра message принимает объект Message и эмулирует его отправку. Вроде все нормально и что-то лучше вряд ли можно придумать. Но у класса Message могут быть классы-наследники. Например, класс EmailMessage для email-сообщений, SmsMessage - для sms-сообщений и так далее
class EmailMessage : Message
{
public EmailMessage(string text) : base(text) { }
}
class SmsMessage : Message
{
public SmsMessage(string text) : base(text) { }
}
Что если мы хотим также отправлять сообщения, которые представляют эти классы? Проблемы вроде нет, поскольку метод SendMessage принимает объект Message и соответственно также и объекты производных классов:
SendMessage(new EmailMessage("Hello World"));
void SendMessage(Message message)
{
Console.WriteLine($"Отправляется сообщение: {message.Text}");
}
Но здесь мы сталкиваемся с преобразованием типов: от EmailMessage к Message. Кроме того, опять же возможна проблема типобезопасности, если мы захотим преобразовать объект message в объект производных классов. И в этом случае чтобы избежать преобразований, мы можем применить обобщения:
void SendMessage<T>(T message)
{
Console.WriteLine($"Отправляется сообщение: {message.Text}"); // ! Ошибка - свойство Text
}
Обобщения позволяют избежать преобразований, но теперь мы сталкиваемся с другой проблемой - универсальный параметр T подразумевает любой тип. Но не любой тип имеет свойство Text. Соответственно свойство Text для объекта типа T не определено и мы не можем это свойство использоваться. Более того для объекта T по умолчанию нам достуны только методы типа object.
Таким образом, возникает проблема: надо избежать преобразований типов и соответственно использовать обобщения, а с другой стороны, необходимо обращаться внутри метода к функционалу класса Message. И ограничения обобщений позволяют решить эту проблему.
Ограничения методов
Ограничения методов указываются после списка параметров после оператора where:
имя_метода<T>(параметры) where T: тип_ограничения
После оператора where указывается универсальный параметр, для которого применяется ограничение. И через двоеточие указывается тип ограничения - обычно в качестве ограничения выступает конкретный тип.
Например, применим ограничения к методу SendMessage, который отправляет объекты Message
SendMessage(new Message("Hello World"));
SendMessage(new EmailMessage("Bye World"));
void SendMessage<T>(T message) where T: Message
{
Console.WriteLine($"Отправляется сообщение: {message.Text}");
}
class Message
{
public string Text { get; } // текст сообщения
public Message(string text)
{
Text = text;
}
}
class EmailMessage : Message
{
public EmailMessage(string text) : base(text) { }
}
Выражение where T: Message в определении метода SendMessage говорит, что через универсальный параметр T будут передаваться объекты класса Message и производных классов. Благодаря этому компилятор будет знать, что T будет иметь функционал класса Message, и соответственно мы сможем обратиться к методам и свойствам класса Message внутри метода без проблем.
При вызове метода нам необязательно указывать тип в угловых скобках - компилятор на основании переданного значения сам определит каким тиом типизиуется метод:
SendMessage(new EmailMessage("Bye World"));
Однако это можно сделать и явно
SendMessage<EmailMessage>(new EmailMessage("Bye World"));
Ограничения обобщений в типах
Подобным образом можно определять и ограничения обобщенных типов. Например, ограничения обобщенных классов:
class имя_класса<T> where T: тип_ограничения
В качестве примера определим класс мессенджера, который будет отправлять сообшения в виде объектов Message:
class Messenger<T> where T : Message
{
public void SendMessage(T message)
{
Console.WriteLine($"Отправляется сообщение: {message.Text}");
}
}
class Message
{
public string Text { get; } // текст сообщения
public Message(string text)
{
Text = text;
}
}
class EmailMessage : Message
{
public EmailMessage(string text) : base(text) { }
}
Здесь для класса Messenger опять же установлено ограничение where T : Message. То есть внутри класса Messenger все объекты типа T можно использовать как объекты Message. И в данном случае в классе Messenger в методе SendMessage опять эмулируется отправка сообшений.
Применим класс для отправки сообщений:
Messenger<Message> telegram = new Messenger<Message>();
telegram.SendMessage(new Message("Hello World"));
Messenger<EmailMessage> outlook = new Messenger<EmailMessage>();
outlook.SendMessage(new EmailMessage("Bye World"));
Типы ограничений и стандартные ограничения
В качестве ограничений мы можем использовать следующие типы:
Классы
Интерфейсы
class - универсальный параметр должен представлять класс
struct - универсальный параметр должен представлять структуру
new() - универсальный параметр должен представлять тип, который имеет общедоступный (public) конструктор без параметров
Есть ряд стандартных ограничений, которые мы можем использовать. В частности, можно указать ограничение, чтобы использовались только структуры или другие типы значений:
class Messenger<T> where T : struct
{}
При этом использовать в качестве ограничения конкретные структуры в отличие от классов нельзя.
Также можно задать в качестве ограничения ссылочные типы:
class Messenger<T> where T : class
{}
А также можно задать с помощью слова new в качестве ограничения класс или структуру, которые имеют общедоступный конструктор без параметров:
class Messenger<T> where T : new()
{}
Если для универсального параметра задано несколько ограничений, то они должны идти в определенном порядке:
Название класса, class, struct. Причем мы можем одновременно определить только одно из этих ограничений
Название интерфейса
new()
class Smartphone<T> where T: Messenger, new()
{
}
Использование нескольких универсальных параметров
Если класс использует несколько универсальных параметров, то последовательно можно задать ограничения к каждому из них:
class Messenger<T, P>
where T : Message
where P: Person
{
public void SendMessage(P sender, P receiver, T message)
{
Console.WriteLine($"Отправитель: {sender.Name}");
Console.WriteLine($"Получатель: {receiver.Name}");
Console.WriteLine($"Сообщение: {message.Text}");
}
}
class Person
{
public string Name { get;}
public Person(string name) => Name = name;
}
class Message
{
public string Text { get; } // текст сообщения
public Message(string text) => Text = text;
}
В данном случае для параметра P будут передаваться объекты типа Person, а для параметра T - объекты Message.
Применим классы:
Messenger<Message, Person> telegram = new Messenger<Message, Person>();
Person tom = new Person("Tom");
Person bob = new Person("Bob");
Message hello = new Message("Hello, Bob!");
telegram.SendMessage(tom, bob, hello);
Консольный вывод:
Отправитель: Tom
Получатель: Bob
Сообщение: Hello, Bob!