OOP Course
Сьогодні

Підрозділ 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 : ТипОбмеження

Типи обмежень узагальнень у C#Типи обмежень узагальнень у C#

Стандартні обмеження: 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 (C# 8) та unmanaged (C# 7.3)

Є ще два додаткових обмеження: notnull (C# 8) і unmanaged (з'явилося раніше, у C# 7.3).

where T : notnull — T не може бути nullable reference type (string?, Patient?) або Nullable<T> (int?, double?). Це корисно в nullable-aware context, де компілятор відстежує nullable-анотації:

where T : unmanaged — T є unmanaged-типом: примітивний числовий тип (int, double, bool, char), enum, покажчик або структура, яка на всіх рівнях вкладеності складається виключно з таких типів (без полів-посилань). Часто такі типи неточно називають blittable, але це різні поняття: bool і char є unmanaged, проте не blittable — при передачі в нативний код їх представлення може змінюватися. Це обмеження дозволяє використовувати unsafe-операції (sizeof(T), поінтер T*, Span<T> зі стек-алокацією), що важливо при роботі з бінарними протоколами та медичним обладнанням (DICOM, HL7 Binary):

Попередня версія цього прикладу використовувала unsafe-код із покажчиками (fixed, *(T*)ptr). Він теж коректний, але вимагає дозволу AllowUnsafeBlocks у проєкті й не виконувався б у браузерному раннері сайту, а ще ставив оголошення структури перед інструкціями верхнього рівня (помилка CS8803). MemoryMarshal з простору імен System.Runtime.InteropServices робить те саме без unsafe — і так само потребує обмеження unmanaged. Перевірено на .NET 10: розмір структури 14 байт, дані відновлюються без втрат.

Зведена таблиця обмежень

Обмеження Що дозволяє 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 Unmanaged value type, без полів-посилань (C# 7.3+) Unsafe-операції, бінарна серіалізація
where T : new() Публічний конструктор без параметрів new T() всередині узагальненого класу
Розроблено Tomka Yurii · © 2026 ·