Числа кажутся самой скучной темой курса ровно до первого отчёта, где сумма не сошлась на копейку. Разберём три ловушки, в которые попадают все.
Целочисленное деление
Тип результата определяется типами операндов, а не тем, что вы ожидали увидеть:
int a = 7;
int b = 2;
Console.WriteLine(a / b); // 3, а не 3.5
Два int дают int. Число 3.5 в целое не помещается, и дробная часть отбрасывается – не
округляется. 9 / 10 даст 0, а не 1.
Чтобы получить дробный результат, дробным должен быть хотя бы один операнд:
Console.WriteLine(7 / 2.0); // 3.5
Console.WriteLine((double)a / b); // 3.5 – привели один операнд к double
Самая частая версия ошибки – double average = sum / count;, где sum и count целые. Деление произойдёт по целочисленным правилам, и только потом результат положат в double. Среднее 3.0 вместо 3.5 – и никаких предупреждений.
Остаток от деления берётся оператором %:
Console.WriteLine(7 % 2); // 1
Console.WriteLine(10 % 5); // 0 – делится нацело
Проверка на чётность через n % 2 == 0 – самое частое его применение.
Дробные числа врут
Это не особенность C#, а свойство двоичной арифметики – в Python и JavaScript будет ровно то же:
double x = 0.1 + 0.2;
Console.WriteLine(x); // 0,3 – выглядит правильно
Console.WriteLine(x == 0.3); // False
Console.WriteLine($"{x:F20}"); // 0,30000000000000004441
x = 0.1 + 0.2
print(x) # 0.30000000000000004
print(x == 0.3) # False
Причина в том, что double хранит число в двоичной системе. Дробь 0.1 в двоичной записи
периодическая – как 1/3 в десятичной. Её приходится обрывать, и накапливается погрешность.
Отсюда два практических правила.
Дробные числа не сравнивают на равенство. Сравнивают модуль разности с допуском:
if (Math.Abs(x - 0.3) < 0.0001)
{
Console.WriteLine("считаем равными");
}
Деньги не хранят в double. Для них есть decimal: он работает в десятичной системе, поэтому
десятичные дроби представляет точно.
decimal a = 0.1m;
decimal b = 0.2m;
Console.WriteLine(a + b == 0.3m); // True
Суффикс m обязателен: без него 0.1 – это double, и компилятор откажется присваивать его
decimal без явного преобразования.
| Тип | Основание | Точность | Для чего |
|---|---|---|---|
float |
двоичное | ~7 знаков | графика, игры, где важнее скорость |
double |
двоичное | ~15 знаков | измерения, физика, вычисления по умолчанию |
decimal |
десятичное | 28 – 29 знаков | деньги, финансы, всё, где считают до копейки |
decimal точнее, но медленнее double в разы и занимает вдвое больше места. Он не «лучший тип для дробей» – он специальный инструмент для десятичных величин.
Переполнение
У int есть верхняя граница, и при её достижении не происходит ничего драматичного. Происходит
кое-что похуже:
int max = int.MaxValue; // 2147483647
Console.WriteLine(max + 1); // -2147483648
Число не выросло, а «завернулось» в минимум – как одометр, перескочивший через нули. Программа не упала, исключения не было: дальше она считает с отрицательным числом и делает это уверенно.
Проверку можно включить блоком checked – тогда вместо тихой чепухи будет честное исключение:
checked
{
int result = max + 1; // OverflowException
}
Держите в голове порядок величин. int – это примерно два миллиарда: количество пользователей переживёт, миллисекунды за век – уже нет. Для больших счётчиков и меток времени берут long, у него запас в четыре миллиарда раз больше.
Приоритет операций и укороченная запись
Порядок действий обычный, школьный: сначала *, /, %, затем + и -, скобки главнее всего.
int result = 2 + 3 * 4; // 14, а не 20
Частые сокращения, которые встречаются в любом коде:
count += 5; // то же, что count = count + 5
count -= 1;
count *= 2;
count++; // увеличить на единицу
count--;
У ++ есть две формы, и разница между ними всплывает в чужом коде постоянно:
int i = 5;
int a = i++; // a = 5, i = 6 – сначала отдал значение, потом увеличился
int b = ++i; // b = 7, i = 7 – сначала увеличился, потом отдал
Писать так самому не стоит: строка, смысл которой зависит от позиции двух плюсов, читается плохо. Но узнавать эту конструкцию нужно.
Что умеет Math
Стандартные операции лежат в классе Math:
Math.Abs(-5); // 5 – модуль
Math.Pow(2, 10); // 1024 – возведение в степень
Math.Sqrt(16); // 4 – корень
Math.Round(3.567, 2); // 3.57 – округление до двух знаков
Math.Floor(3.9); // 3 – вниз
Math.Ceiling(3.1); // 4 – вверх
Math.Max(5, 9); // 9
Отдельного оператора возведения в степень в C# нет – в отличие от Python с его **. Для квадрата
обычно пишут x * x: короче и быстрее, чем Math.Pow(x, 2).
Math.Round по умолчанию округляет половину к чётному: и 2.5, и 3.5 дают 3. Это не ошибка, а банковское округление – оно не накапливает перекос при суммировании. Привычное «половина вверх» включается вторым аргументом: Math.Round(2.5, MidpointRounding.AwayFromZero).