Підрозділ 4.9
Обмеження узагальнень
Розглядає обмеження generics через where, обмеження методів і класів, стандартні class, struct, new() та кілька параметрів.
4.9. Обмеження узагальнень
Узагальнені типи дозволяють використовувати будь-який тип як параметр. Але іноді всередині узагальненого класу чи методу необхідно звертатися до конкретних членів об'єкта — властивостей, методів. Компілятор не дозволить цього, якщо параметр T нічим не обмежений, бо у такому разі T може бути будь-яким типом, і немає гарантії, що потрібний член існує.
Обмеження узагальнень (generic constraints) через ключове слово where дозволяють вказати, яким типам відповідає параметр T. Це дає компілятору достатньо інформації для перевірки коректності звернень до членів.
Проблема без обмежень
Розглянемо клінічний приклад. У нас є клас Notification з властивістю Text, і ми хочемо написати узагальнений метод Send<T>:
Якщо написати void Send<T>(T message) { Console.WriteLine(message.Text); } — компілятор видасть помилку: T може бути чим завгодно, і не факт, що в нього є Text. Саме тут на допомогу приходять обмеження.
Обмеження методу: where T : ТипКласу
Обмеження методу вказується після списку параметрів через where:
Вираз where T : Notification каже компілятору: «T — це завжди Notification або похідний від нього клас». Тому звернення до message.Text стає безпечним і дозволеним. При виклику Send(new SmsNotification(...)) компілятор сам визначає T = SmsNotification — явно вказувати тип не обов'язково.
Обмеження класу: where T : ТипКласу
Так само обмеження застосовуються до узагальнених класів. Синтаксис:
class Ім'яКласу<T> where T : ТипОбмеження
Стандартні обмеження: class, struct, new()
Окрім конкретних класів та інтерфейсів, є вбудовані обмеження:
where T : class — T має бути reference type (клас, інтерфейс, рядок):
where T : struct — T має бути value type (int, double, struct і т.д.):
where T : new() — T має публічний конструктор без параметрів. Це дозволяє всередині узагальненого класу створювати екземпляри типу T через new T():
Обмеження за інтерфейсом
Як обмеження можна вказати інтерфейс — тоді T гарантовано реалізує всі члени цього інтерфейсу. Це дозволяє звертатися до методів інтерфейсу всередині узагальненого класу:
IComparable<T> гарантує наявність методу CompareTo(), тому сортування працює для будь-якого типу, що його реалізує: int, string, DateTime, або власний клас.
Кілька обмежень одночасно
Для одного параметра можна задати кілька обмежень. Вони вказуються у строгому порядку: спочатку клас або class/struct, потім інтерфейси, в кінці new():
where TPatient : Patient, new() означає: TPatient повинен бути Patient або похідним класом і мати публічний конструктор без параметрів. Для різних параметрів типу обмеження задаються окремими рядками where.
Порядок та правила обмежень
Кілька правил, які слід пам'ятати:
- Одночасно можна вказати лише одне з: конкретний клас,
class,struct(вони несумісні між собою) - Інтерфейсів може бути кілька
new()завжди стоїть останнімstructнесумісне зnew()— структури за визначенням мають конструктор без параметрів
// Правильно:
void Process<T>(T item) where T : Notification, new() { }
// Правильно — кілька інтерфейсів:
void Compare<T>(T a, T b) where T : class, IComparable<T>, IEquatable<T> { }
// Помилка — не можна одночасно class і struct:
// void Wrong<T>() where T : class, struct { }Обмеження notnull та unmanaged (C# 8+)
Починаючи з C# 8 з'явилися два додаткових обмеження.
where T : notnull — T не може бути nullable reference type (string?, Patient?) або Nullable<T> (int?, double?). Це корисно в nullable-aware context, де компілятор відстежує nullable-анотації:
where T : unmanaged — T є blittable value type: примітивний тип (int, double, bool, char) або структура, яка складається виключно з таких типів (без полів-посилань). Це обмеження дозволяє використовувати unsafe-операції (sizeof(T), поінтер T*, Span<T> зі стек-алокацією), що важливо при роботі з бінарними протоколами та медичним обладнанням (DICOM, HL7 Binary):
Зведена таблиця обмежень
| Обмеження | Що дозволяє T | Типовий сценарій |
|---|---|---|
where T : ClassName |
T — цей клас або похідний | Доступ до членів базового класу |
where T : IInterface |
T реалізує інтерфейс | Виклик методів контракту |
where T : class |
Reference type (клас, рядок) | Можна порівнювати з null |
where T : struct |
Value type (struct, int…) | T ніколи не null |
where T : notnull |
Не nullable (C# 8+) | Nullable-safe контейнери |
where T : unmanaged |
Blittable value type (C# 7.3+) | Unsafe-операції, бінарна серіалізація |
where T : new() |
Публічний конструктор без параметрів | new T() всередині узагальненого класу |