Программа курса← C#: Базовый курс
1Знакомство с C# и .NET
2Данные и типы
3Ветвления и циклы
4Методы и ошибки
5Коллекции и строки
6Классы и объекты

Числа и точность

Числа кажутся самой скучной темой курса ровно до первого отчёта, где сумма не сошлась на копейку. Разберём три ловушки, в которые попадают все.

Целочисленное деление

Тип результата определяется типами операндов, а не тем, что вы ожидали увидеть:

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).