Оператор принимает один, два или три операнда и выдаёт значение: a + b, !done, x ?? fallback. Операторы C# близки к операторам C, Java и JavaScript, но некоторые из них ведут себя неожиданно для новичков, прежде всего целочисленное деление и знак результата %.
Арифметические операторы
+, -, *, / и % работают с любым числовым типом. Тип результата следует за операндами: два значения int дают int, int и double дают double.
Вывод:
22
12
85
3
2
3.4
3.4
3
3.4
Целочисленное деление отбрасывает дробную часть в сторону нуля. 17 / 5 равно 3, а -17 / 5 равно -3. Это причина очень частой ошибки: int percent = done / total * 100; равно 0 всякий раз, когда done < total, потому что done / total уже равно 0. Сначала умножайте (done * 100 / total) или делите как double.
Деление целого числа или decimal на ноль выбрасывает DivideByZeroException. Деление double на ноль исключения не выбрасывает; получается бесконечность или NaN для 0.0 / 0.0:
Вывод:
True
True
False
DivideByZeroException
Остаток от деления и отрицательные числа
% возвращает остаток от деления, и его результат получает знак левого операнда. Это отличается от Python и от математического «mod», которые при положительном делителе всегда неотрицательны:
Вывод:
1
-1
1
2
even
62 min 5 s
Thu
1.5
Используйте вспомогательный метод Mod всегда, когда левая часть может быть отрицательной, а результат служит индексом: days[(today + offset) % 7] выбросит IndexOutOfRangeException при отрицательной сумме. У проверки на нечётность та же ловушка: n % 2 == 1 равно false для отрицательных нечётных чисел; проверяйте n % 2 != 0.
Присваивание, составное присваивание и инкремент
= присваивает. Составные формы +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= объединяют операцию с присваиванием. ++ и -- прибавляют или вычитают 1 и бывают в префиксной и постфиксной формах, которые различаются возвращаемым значением:
Вывод:
14
start, step
a=5 b=7 i=7
4
Случай с byte показывает особенность: level = level + 10; не компилируется, потому что byte + int даёт int, а level += 10; компилируется, потому что составное присваивание включает неявное приведение обратно к типу переменной. Это приведение при переполнении переходит через границу (250 + 10 превращается в 4).
Операторы сравнения и логические операторы
Операторы сравнения (==, !=, <, >, <=, >=) возвращают bool. Для строк == сравнивает текст, а не ссылки. Логические операторы: ! (не), && (и), || (или) и ^ (исключающее или).
Вывод:
True
no active user
with ||:
evaluated A
with |:
evaluated A
evaluated B
True
&& и || работают по короткой схеме: правый операнд выполняется, только если он может изменить результат. Именно поэтому user != null && user.IsActive безопасно. Одиночные & и | для логических значений всегда вычисляют обе части, а в условии это редко то, что нужно.
Побитовые операторы и сдвиги
Для целочисленных типов &, |, ^ и ~ работают с отдельными битами, а << и >> сдвигают их. Их используют для флагов, масок, хеширования и низкоуровневых протоколов:
Вывод:
3
True
False
101
1024
125
5
-1
Для именованных наборов флагов перечисление с атрибутом [Flags] понятнее целочисленных констант, а HasFlag читается лучше маски. C# 11 добавил >>>, беззнаковый сдвиг вправо, который заполняет освободившиеся биты нулями даже для отрицательных чисел.
Операторы для null
Для работы со значениями, которые могут быть null, есть три оператора:
Вывод:
Guest
0
5
a ?? b возвращает a, если оно не null, иначе b. a?.Member возвращает null вместо исключения, когда a равно null. C# 8 добавил a ??= b, который присваивает b переменной a только тогда, когда a равно null. Подробно они разобраны в разделе типы, допускающие null.
Приоритет операторов
Когда в выражении смешаны операторы, строки выше связывают сильнее. Операторы одной строки группируются слева направо, кроме присваивания, ?? и ?:, которые группируются справа налево. (Сами операнды всегда вычисляются слева направо.)
| Категория | Операторы |
|---|---|
| Первичные | x.y, x?.y, f(x), a[i], x++, x--, new, typeof, nameof |
| Унарные | +x, -x, !x, ~x, ++x, --x, (T)x, await |
| Мультипликативные | *, /, % |
| Аддитивные | +, - |
| Сдвиг | <<, >>, >>> |
| Отношения и проверка типа | <, >, <=, >=, is, as |
| Равенство | ==, != |
| Побитовое И | & |
| Побитовое исключающее ИЛИ | ^ |
| Побитовое ИЛИ | | |
| Условное И | && |
| Условное ИЛИ | || |
| Объединение с null | ?? |
| Условный оператор | c ? a : b |
| Присваивание и лямбда | =, +=, -=, ??=, => и другие составные формы |
Два следствия, которые стоит знать: a + b * c сначала умножает, а x & mask == 0 сначала сравнивает (== связывает сильнее, чем &), поэтому означает x & (mask == 0) и не компилируется для целых чисел. Пишите (x & mask) == 0. Скобки ничего не стоят и делают намерение очевидным.
Перегрузка операторов
Ваши собственные типы могут определять, что означают операторы. Именно так decimal, DateTime и TimeSpan поддерживают + и <:
Вывод:
33.49
True
Операторы должны быть public static, а некоторые обязательно определяются парами: определение < требует >, а == требует !=. Если определён ==, компилятор также выдаёт предупреждение, пока вы не переопределите Equals и GetHashCode согласованно. Перегружайте операторы только там, где их смысл очевиден, например для чисел, векторов и денег.
Часто задаваемые вопросы
Что делает % в C#?
% это оператор остатка от деления: 17 % 5 равно 2. Результат имеет знак левого операнда, поэтому -7 % 3 равно -1, а не 2, как в Python. Чтобы результат всегда был неотрицательным, как нужно при циклическом переборе индекса, пишите ((a % n) + n) % n. Оператор работает и с double и decimal: 7.5 % 2 равно 1.5.
Почему 7 / 2 равно 3 в C#?
Когда оба операнда целые, / выполняет целочисленное деление и отбрасывает дробную часть в сторону нуля. Сделайте один из операндов числом с плавающей точкой или decimal, чтобы получить дробный результат: 7 / 2.0 равно 3.5, (double)a / b преобразует перед делением, а 7m / 2 даёт 3.5 типа decimal.
Чем && отличается от & в C#?
&& это условное И: если левая часть равна false, правая не вычисляется. & для логических значений всегда вычисляет обе части, а для целых чисел это побитовое И. Используйте && и || в условиях, особенно в проверках вроде user != null && user.IsActive, которая с & выбросила бы NullReferenceException.
Чем i++ отличается от ++i в C#?
Оба прибавляют 1 к i. Выражение i++ возвращает старое значение, ++i новое. В отдельной строке (i++;) разницы нет. Она важна только тогда, когда результат используется, как в int a = i++;, поэтому многие руководства по стилю держат инкременты в отдельной строке.
Какой приоритет операторов в C#?
От высшего к низшему: первичные (x.y, f(x), a[i], x++), унарные (!, -, ++x, приведения), мультипликативные (* / %), аддитивные (+ -), сдвиги, отношения (< > <= >= is as), равенство (== !=), &, ^, |, &&, ||, ??, условный ?: и в конце присваивание. Если сомневаетесь, добавьте скобки.