Перегрузка операторов в C# — это объявление в своем типе метода public static с ключевым словом operator, который задает, что делают +, ==, * и другие операторы с объектами этого типа. После этого выражение a + b для двух векторов компилятор превращает в вызов вашего метода.
Содержание
Рядом стоят три близких понятия, которые легко спутать:
- перегрузка оператора — новый смысл существующего символа (
+,==) для вашего типа; - оператор преобразования (
implicit/explicit) — правило, как превратить ваш тип в другой и обратно; символа операции у него нет, это приведение типа; - перегрузка метода — несколько методов с одним именем и разными параметрами; к операторам отношения не имеет, хотя внутри компилятор и хранит операторы как методы (
op_Addition,op_Equality).
Все примеры ниже проверены на .NET SDK 10.0.401 (C# 14) 24.09.2026, вывод скопирован из прогона.
Минимальный рабочий пример
Возьмем двумерный вектор: его естественно складывать, умножать на число и сравнивать. Код целиком — это консольная программа с top-level statements.
using System;
var a = new Vector2(1, 2);
var b = new Vector2(3, 4);
Console.WriteLine(a + b); // бинарный +
Console.WriteLine(-a); // унарный -
Console.WriteLine(a * 2); // вектор * число
Console.WriteLine(2 * a); // число * вектор
Console.WriteLine(a == new Vector2(1, 2));
Console.WriteLine(a != b);
Vector2 c = (5.0, 6.0); // implicit: кортеж -> Vector2
double len = (double)b; // explicit: Vector2 -> double (длина)
Console.WriteLine(c);
Console.WriteLine(len);
var p = a;
p++; // один оператор ++ на префикс и постфикс
Console.WriteLine(p);
public readonly struct Vector2 : IEquatable<Vector2>
{
public double X { get; }
public double Y { get; }
public Vector2(double x, double y) { X = x; Y = y; }
public static Vector2 operator +(Vector2 l, Vector2 r) => new(l.X + r.X, l.Y + r.Y);
public static Vector2 operator -(Vector2 v) => new(-v.X, -v.Y);
public static Vector2 operator ++(Vector2 v) => new(v.X + 1, v.Y + 1);
public static Vector2 operator *(Vector2 v, double k) => new(v.X * k, v.Y * k);
public static Vector2 operator *(double k, Vector2 v) => v * k;
public static bool operator ==(Vector2 l, Vector2 r) => l.Equals(r);
public static bool operator !=(Vector2 l, Vector2 r) => !l.Equals(r);
public static implicit operator Vector2((double X, double Y) t) => new(t.X, t.Y);
public static explicit operator double(Vector2 v) => Math.Sqrt(v.X * v.X + v.Y * v.Y);
public bool Equals(Vector2 other) => X.Equals(other.X) && Y.Equals(other.Y);
public override bool Equals(object? obj) => obj is Vector2 v && Equals(v);
public override int GetHashCode() => HashCode.Combine(X, Y);
public override string ToString() => $"({X}; {Y})";
}
В консоли появятся девять строк: сумма, противоположный вектор, два одинаковых произведения, два True, вектор из кортежа, длина вектора (3; 4), равная 5, и результат инкремента.
(4; 6)
(-1; -2)
(2; 4)
(2; 4)
True
True
(5; 6)
5
(2; 3)
Синтаксис operator по шагам
Общая форма для бинарного оператора: public static <тип результата> operator <символ>(<тип1> левый, <тип2> правый). Для унарного — один параметр. Разберем, что в примере обязательно, а что — выбор автора.
public staticобязательны. Оператор не вызывается на экземпляре, оба операнда приходят параметрами. Исключение появилось в C# 14 — составные операторы вроде+=и экземплярные++/--(о них ниже).- Хотя бы один параметр должен быть вашим типом. Нельзя переопределить
+для двухdecimalизнутри своей структуры: компилятор ответитerror CS0563: One of the parameters of a binary operator must be the containing type. - Порядок операндов важен.
a * 2и2 * a— две разные сигнатуры, поэтому в примере два оператора*. Второй просто вызывает первый, чтобы не дублировать логику. - Тип результата — любой разумный.
+возвращает новый вектор,==—bool, а оригинальные объекты не меняются. Дляreadonly structэто единственный возможный вариант, для класса — хорошая практика. - Приоритет, ассоциативность и число операндов изменить нельзя.
a + b * cвсегда посчитает умножение первым, как для чисел. Новых символов (например,**) тоже не придумать.
Какие операторы можно перегрузить
| Группа | Операторы | Правило |
|---|---|---|
| Унарные | + - ! ~ ++ --, true false |
++/-- одним методом на обе формы |
| Бинарные | + - * / % & | ^ << >> >>> |
>>> — с C# 11 |
| Сравнение | == !=, < >, <= >= |
только парами |
| Составные | += -= *= и т.п. |
до C# 14 выводятся из +; с C# 14 можно объявить свои |
| Не перегружаются | && ||, =, ., ?:, ??, [], () |
&& строится из & и true/false, [] — через индексатор, приведение — через implicit/explicit |
Про && и || деталь важная: если в типе есть &, |, true и false, компилятор сам соберет из них && и || с сокращенным вычислением. В проверочном прогоне правый операнд при false && ... действительно не вычислялся.
Парные операторы и равенство
== без != не скомпилируется. Возьмем структуру, где автор забыл пару:
using System;
Console.WriteLine(new Money(5) == new Money(5));
public readonly struct Money
{
public decimal Amount { get; }
public Money(decimal amount) => Amount = amount;
public static bool operator ==(Money l, Money r) => l.Amount == r.Amount;
}
Сборка падает, и компилятор заодно предупреждает про Equals и GetHashCode:
error CS0216: The operator 'Money.operator ==(Money, Money)' requires a matching operator '!=' to also be defined
warning CS0660: 'Money' defines operator == or operator != but does not override Object.Equals(object o)
warning CS0661: 'Money' defines operator == or operator != but does not override Object.GetHashCode()
Исправление — то, что сделано в Vector2: добавить !=, переопределить Equals(object) и GetHashCode() и реализовать IEquatable<T>. Причина не формальная: Dictionary и HashSet сравнивают ключи через Equals и GetHashCode, а не через ваш ==. Если они расходятся, объект, равный по ==, не найдется в словаре.
Если нужен тип с равенством по значениям полей, проще объявить record struct или record: компилятор сам сгенерирует ==, !=, Equals и GetHashCode.
Ловушка: == null внутри своего ==
Для классов (ссылочных типов) частая ошибка — проверить null тем же оператором, который пишешь:
public static bool operator ==(Node? l, Node? r)
{
if (l == null) return r == null; // ошибка: вызывает этот же оператор
return r is not null && l.Value == r.Value;
}
l == null снова вызывает operator ==, тот снова себя — и процесс падает:
Stack overflow.
Repeated 130981 times:
--------------------------------
at Node.op_Equality(Node, Node)
--------------------------------
Число повторов зависит от размера стека и может отличаться. Исправление — if (l is null) return r is null;: паттерн is null не вызывает пользовательские операторы. После замены та же программа печатает True.
Операторы преобразования: implicit и explicit
Преобразование объявляется так же, public static, но вместо символа операции стоит целевой тип: implicit operator Vector2(...) или explicit operator double(...). Выбор между ними — по одному вопросу: может ли преобразование потерять данные или выбросить исключение.
| Вид | Как вызывается | Когда выбирать |
|---|---|---|
implicit |
Vector2 c = (5.0, 6.0); без приведения |
всегда успешно, без потерь |
explicit |
(double)b только явным приведением |
теряет информацию (вектор -> длина) или может упасть |
Если объявлен только explicit, неявное присваивание не соберется:
var t = new Temperature(36.6m);
decimal d = t; // нет implicit-преобразования
error CS0266: Cannot implicitly convert type 'Temperature' to 'decimal'. An explicit conversion exists (are you missing a cast?)
Исправление — decimal d = (decimal)t;. Делать преобразование неявным ради краткости не стоит: скрытые implicit затрудняют чтение кода и выбор перегрузок методов.
++, checked и новое в C# 14
Инкремент. В C# статический operator ++ один на префиксную и постфиксную форму: он возвращает новое значение, а компилятор сам решает, какое значение выражения отдать (до или после). Фиктивного параметра int, как в C++, нет.
checked-операторы (C# 11). Можно объявить две версии: обычную и operator checked +. Вторая вызывается внутри checked(...):
public static Counter operator +(Counter l, Counter r) => new(unchecked(l.Value + r.Value));
public static Counter operator checked +(Counter l, Counter r) => new(checked(l.Value + r.Value));
Для Counter(int.MaxValue) + Counter(1) обычная версия вернула -2147483648, а checked(...) выбросила OverflowException.
Составные операторы (C# 14). Раньше x += y всегда разворачивалось в x = x + y, то есть создавался новый объект. Теперь для изменяемого класса можно объявить экземплярный оператор, который меняет объект на месте:
using System;
var buf = new Accumulator();
buf += 10;
buf += 5;
Console.WriteLine(buf.Total); // 15
public class Accumulator
{
public int Total { get; private set; }
public void operator +=(int x) => Total += x;
}
Такие операторы, как и экземплярные ++/-- из C# 14 (public void operator ++()), не static и возвращают void. Для неизменяемых типов вроде Vector2 он не нужен: += по-прежнему работает через +.
C# и C++: сравнение в одной таблице
Подробный разбор перегрузки в C++ — тема отдельного материала; здесь только различия, которые важны при переходе с одного языка на другой.
| Аспект | C# | C++ |
|---|---|---|
| Форма | только public static метод (кроме составных в C# 14) |
метод класса или свободная функция |
= присваивание |
не перегружается | перегружается (копирование, перемещение) |
[], (), -> |
индексатор, делегаты; -> нет |
перегружаются как методы класса |
++ постфикс |
один метод на обе формы | отдельная сигнатура с фиктивным int |
&&, || |
через &/| + true/false, сокращенное вычисление сохраняется |
перегружаются напрямую, сокращенное вычисление теряется |
| Пары сравнения | ==/!=, </>, <=/>= обязательны парами |
не обязательны; с C++20 <=> и == генерируют остальные |
| Преобразования | implicit/explicit operator T |
operator T(), explicit — по желанию |
new/delete |
не перегружаются | перегружаются |
Главное смысловое различие: C# ограничивает перегрузку, чтобы код оставался предсказуемым, а C++ дает почти полный контроль, включая управление памятью.
Когда перегрузка уместна
Перегружайте операторы, если у операции есть общепринятый смысл для вашего типа: векторы, матрицы, комплексные числа, деньги в одной валюте, даты и интервалы. Если читателю кода придется гадать, что значит order + customer, лучше написать метод с понятным именем. Для обобщенных алгоритмов в .NET 7+ есть интерфейсы generic math (IAdditionOperators<TSelf, TOther, TResult> и другие): тип с операторами может реализовать их, и тогда один метод будет работать и с int, и с вашим Vector2.
Выводы
- Оператор в C# — это
public staticметод с ключевым словомoperator, где хотя бы один параметр — ваш тип. ==/!=,</>,<=/>=объявляются только парами; вместе с==нужно переопределитьEqualsиGetHashCode.implicit— для преобразований без потерь,explicit— когда данные теряются или возможна ошибка.- Внутри собственного
==проверятьnullчерезis null, иначе бесконечная рекурсия. - В отличие от C++, в C# нельзя перегрузить
=,[],new; зато&&/||сохраняют сокращенное вычисление, а с C# 14 появились составные операторы на месте.
Где применяется / связь с практикой
Освойте тему на практике
Перегрузка операторов встречается в библиотеках математики и геометрии, в игровых движках (векторы и матрицы в Unity), в доменных моделях с деньгами и единицами измерения. На собеседованиях по C# часто спрашивают про пару ==/Equals/GetHashCode и разницу implicit и explicit. Системно разобрать язык, generic math и проектирование типов можно на курсе «C#-разработчик. Продвинутый уровень», а отдельные темы по .NET — на бесплатных открытых уроках Otus.
FAQ
Можно ли перегрузить оператор в интерфейсе?
Да, с C# 11 интерфейс может объявить static abstract оператор, который обязан реализовать тип. На этом построены интерфейсы generic math в .NET 7 и новее.
Почему операторы нельзя сделать virtual?
Они статические, поэтому выбор перегрузки идет по типам на этапе компиляции. Если нужна полиморфная логика, оператор вызывает виртуальный метод, например Equals.
Как вызвать оператор из VB.NET или через рефлексию?
Компилятор создает методы со служебными именами: op_Addition для +, op_Equality для ==, op_Implicit и op_Explicit для преобразований. VB.NET и F# распознают эти методы и позволяют писать обычное a + b, а по имени их ищут через рефлексию (typeof(Vector2).GetMethod("op_Addition")) или вызывают из языков без поддержки операторов.



