Підрозділ 8.1
Визначення операторів
Пояснює перевантаження операторів у класах і структурах, синтаксис operator, бінарні оператори, оператори порівняння та обмеження.
8.1. Визначення операторів
Оператори +, -, >, <, == добре знайомі у контексті примітивних типів: int, double, bool. Але коли ми створюємо власний клас, компілятор не знає, що означає «скласти» або «порівняти» два об'єкти — адже у кожного класу своя семантика. Перевантаження операторів (operator overloading) — це механізм, який дозволяє визначити власну логіку для стандартних операторів стосовно об'єктів нашого типу.
Уявіть клінічну систему, де потрібно порівнювати температуру тіла двох пацієнтів або відстежувати її зміну після прийому жарознижуючого. Без перевантаження код виглядає громіздко і прив'язаний до деталей реалізації:
if (patient1.Temperature.Celsius > patient2.Temperature.Celsius) { ... }
double change = afterMed.Temperature.Celsius - beforeMed.Temperature.Celsius;З перевантаженням — читабельно і природно, як у математиці:
if (tempAfter > tempBefore) { ... }
double change = tempAfter - tempBefore;Перевантаження операторів не змінює їх пріоритет, не додає нових символів і не змінює правил мови — воно лише визначає, що відбувається, коли стандартний символ застосовується до об'єктів нашого типу.
Механізм: як компілятор знаходить оператор
Коли компілятор зустрічає вираз t1 + delta, де t1 має тип BodyTemperature, він шукає у цьому класі статичний метод із підходящою сигнатурою:
public static BodyTemperature operator+(BodyTemperature t, double delta)Якщо такий метод є — вираз t1 + delta автоматично перетворюється на виклик цього статичного методу. У скомпільованому коді він має спеціальне ім'я — op_Addition (для - — op_Subtraction, для > — op_GreaterThan, для == — op_Equality тощо), тож фактично виконується BodyTemperature.op_Addition(t1, delta). Ці імена видно через рефлексію і використовують мови .NET, що не мають синтаксису операторів; з C# так викликати метод не можна — лише через сам оператор. Якщо відповідного методу немає — помилка компіляції CS0019. Це і є «синтаксичний цукор»: зручний запис оператором, за яким приховується звичайний виклик статичного методу.


Синтаксис визначення оператора
Оператор визначається як спеціальний метод у тілі класу або структури:
public static ТипРезультату operator СИМВОЛ(параметр1[, параметр2])
{
// логіка
}Обов'язкові вимоги:
- Модифікатори
public static— оператор є методом типу, а не конкретного екземпляра. - Ключове слово
operatorперед символом оператора. - Принаймні один параметр повинен мати тип класу або структури, де визначається оператор.
- Бінарні оператори (два операнди) — приймають два параметри.
- Унарні оператори (один операнд) — приймають один параметр.
- Тип результату може бути будь-яким: тим самим класом,
bool,double, будь-яким іншим.
Клінічний приклад: клас BodyTemperature
Розглянемо клас BodyTemperature, що представляє температуру тіла пацієнта. Цей клас — наочний приклад для перевантаження: температури порівнюють (чи є жар?), обчислюють різницю (наскільки знизилась?), застосовують зсув (зросла на 0.5°C).
Арифметичні оператори
Визначимо два оператори: + для зсуву температури на задану величину і - для обчислення різниці між двома вимірами. Зверніть увагу, що вони повертають різні типи — BodyTemperature і double відповідно, адже семантика операцій різна:
Тип результату оператора — не обов'язково той самий клас. Оператор - між двома BodyTemperature повертає double, бо різниця температур — числова величина, а не нова температура. Тип результату визначається семантикою операції.
Оскільки в операторі + перший параметр — BodyTemperature, а другий — double, вираз morning + 0.8 коректний. Але 0.8 + morning вже не компілюється — параметри не збігаються в потрібному порядку. Якщо потрібно, можна визначити ще один перевантажений варіант із переставленими параметрами.
Оператори порівняння
Порівняння — найчастіший сценарій у клінічних системах: «чи є у пацієнта жар?», «яка температура вища?». Визначимо чотири оператори порівняння:
Зверніть увагу на оператор ==: замість строгого t1.Celsius == t2.Celsius використовується допуск 0.05. Це важливо для чисел із плаваючою крапкою — через особливості їх бінарного представлення два «однакові» значення після арифметичних операцій можуть трохи відрізнятись.
Повний клас у клінічному сценарії
Об'єднаємо всі оператори в повному класі і побачимо, як природно вони виглядають у реальному коді:
Парні оператори
Деякі оператори компілятор вимагає визначати парами. Якщо ви визначаєте один — необхідно визначити і другий:
| Пара | Причина |
|---|---|
> і < |
Логічна симетрія: якщо a > b, то b < a |
>= і <= |
Аналогічно |
== і != |
Якщо об'єкти рівні, то вони не нерівні |
Ця вимога вбудована в компілятор — він не дозволить визначити > без <. Мета обмеження — запобігти логічним суперечностям: ситуації, коли визначено лише a > b, а вираз b < a для того самого типу не компілюється, або коли a == b і a != b одночасно повертають true. Ту саму вимогу парності компілятор ставить і до операторів true/false (розділ 8.2). Зауважимо, що компілятор перевіряє лише наявність пари, але не узгодженість реалізацій — логіку обох операторів програміст пише сам.
При перевантаженні == компілятор також рекомендує перевизначити Equals(object) і GetHashCode(). Причина: за замовчуванням ці методи порівнюють посилання, а не значення. Якщо залишити їх без змін, поведінка стає непослідовною — == порівнює значення, а Equals() — посилання на об'єкт у пам'яті.
Таблиця операторів, що підтримують перевантаження
| Категорія | Оператори | Примітка |
|---|---|---|
| Унарні | +x, -x, !, ~, ++, --, true, false |
true/false — для умовних виразів |
| Арифметичні | +, -, *, /, % |
— |
| Порівняння | ==, !=, <, >, <=, >= |
Тільки парами |
| Порозрядні | &, |, ^, <<, >>, >>> |
— |
| Логічні | &&, || |
Визначаються через &, |, true, false |
Оператор присвоєння =, тернарний ?:, оператори доступу . та ?., ??, а також && і || напряму перевантажити неможливо (два останні працюють через &, |, true, false). Складені присвоєння +=, -= тощо за замовчуванням автоматично використовують перевантажений + і - — визначати їх окремо не потрібно: t += 0.5 компілюється у t = t + 0.5, тобто створює новий об'єкт.
Нове в C# 14 (.NET 10). Для типів, де створення нового об'єкта на кожну операцію дороге (великі буфери, вектори, матриці), C# 14 дозволяє оголосити складене присвоєння окремим екземплярним оператором, що змінює об'єкт на місці:
class DoseAccumulator
{
public double TotalMg { get; private set; }
// C# 14: екземплярний оператор, без static, повертає void
public void operator +=(double mg) => TotalMg += mg;
}Аналогічно можна оголосити екземплярні ++ і -- (public void operator ++()). Якщо такого оператора немає — працює старе правило через +. Перевірено на .NET 10: acc += 2 для такого класу змінює той самий об'єкт.
Коли варто, а коли не варто перевантажувати оператори
Варто, якщо:
- Тип моделює математичне або фізичне поняття: температура, відстань, маса, координата, інтервал часу.
- Оператор має очевидну і однозначну інтерпретацію для цього типу.
- Код з оператором читається так само природно, як математичний вираз.
Не варто, якщо:
- Семантика оператора неочевидна або може трактуватись по-різному.
- Ви перевантажуєте оператор виключно заради стислості — без виграшу в читабельності.
- Тип не є «значеннєвим» поняттям. Для сутностей-ідентичностей — пацієнт, лікар, прийом — оператор
+між двома об'єктами не має природного змісту.
Перевантаження операторів — інструмент виразності, а не синтаксичний трюк. Добре спроектований оператор робить код читабельнішим; поганий вносить плутанину і ускладнює підтримку.