С помощью универсальных параметров мы можем типизировать обобщенные классы любым типом. Однако иногда возникает необходимость конкретизировать тип. Например, у нас есть следующий класс 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!