Ограничения обобщений

Последнее обновление: 29.12.2021

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

Если для универсального параметра задано несколько ограничений, то они должны идти в определенном порядке:

  1. Название класса, class, struct. Причем мы можем одновременно определить только одно из этих ограничений

  2. Название интерфейса

  3. 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!
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850