double и decimal
Целые числа C# хранит точно, пока они помещаются в тип. С дробными числами иначе, и это видно уже на сложении двух десятых.
Наберите на калькуляторе 1 ÷ 3. На экране появится 0,33333333: восемь троек, и место кончилось. Настоящая треть — бесконечная дробь, а калькулятор хранит только её начало.
Тип double устроен похоже, только хранит числа в двоичной системе. В ней число записывают всего двумя цифрами, 0 и 1. В двоичной системе даже 0.1 — бесконечная дробь, как треть в десятичной. Поэтому double хранит не саму 0.1, а ближайшее к ней число, которое помещается в тип:
double sum = 0.1 + 0.2;
Console.WriteLine(sum); // 0.30000000000000004
Console.WriteLine(sum == 0.3); // FalseДавайте разберём, что произошло:
- В
0.1и0.2уже есть крошечные погрешности, и при сложении они накапливаются. - Сумма печатается как
0.30000000000000004: C# показывает столько цифр, сколько нужно, чтобы отличить это число от других значенийdouble, в том числе от0.3. - Сравнение
sum == 0.3печатаетFalse: вsumи в литерале0.3лежат разные числа.
Как вы помните из темы о типах, с русскими региональными настройками первая строка выглядит иначе: 0,30000000000000004.
Здесь аналогия с калькулятором заканчивается. Калькулятор показывает, где обрезал дробь, а double печатает 0.1 как 0.1 и прячет погрешность, пока та не проявится после вычислений.
Этот вопрос часто задают на собеседовании: почему 0.1 + 0.2 не равно 0.3? Ответ: double хранит двоичную дробь, а 0.1 и 0.2 нельзя точно записать двоичными цифрами.
Сравнение с допуском
Из-за погрешности числа double не сравнивают через ==. Сравнивают разницу между ними: если она меньше крошечного допуска, числа считают равными. Выражение Math.Abs(sum - 0.3) < 1e-9 даёт true. Разберём его по частям:
sum - 0.3— разница двух чисел;Math.Absубирает у разницы знак, ведь она может оказаться и отрицательной;< 1e-9проверяет, что разница меньше допуска;1e-9— так в коде пишут число0.000000001: единица, делённая на десять в девятой степени.
decimal для денег
В бухгалтерии сумма должна сходиться до копейки. Поэтому деньги хранят в типе decimal: он записывает число десятичными цифрами, и 0.1 в нём хранится точно.
decimal total = 0.1m + 0.2m;
Console.WriteLine(total); // 0.3
Console.WriteLine(total == 0.3m); // TrueБуква m делает литерал decimal: без неё 0.1 был бы double. Чем отличаются два типа:
double | decimal | |
|---|---|---|
| Как хранит число | двоичными цифрами | десятичными цифрами |
| Значащих цифр | 15–17 | 28–29 |
| Скорость вычислений | выше | ниже |
| Для чего подходит | измерения, физика, графика | деньги и точный учёт |
decimal точен только для десятичных дробей. Треть не записывается и десятичными цифрами, поэтому 1m / 3 даёт 0.3333333333333333333333333333 — дробь, округлённую до 28 знаков после точки.
Частая ошибка
Ставку налога объявили как double и умножили на неё цену типа decimal:
decimal price = 19.99m;
double rate = 0.2;
decimal tax = price * rate; // ✗
Console.WriteLine(tax);error CS0019: Operator '*' cannot be applied to operands of type 'decimal' and 'double'Программа не компилируется. C# не смешивает double и decimal в одной операции: сам он не превращает один из этих типов в другой ни в какую сторону. Объявите ставку как decimal rate = 0.2m;, и tax станет 3.998.
Главное
doubleхранит двоичную дробь, поэтому0.1 + 0.2не равно0.3; такие числа сравнивают с допуском.decimalхранит десятичные цифры и точно считает деньги; литералы для него пишут с суффиксомm.doubleиdecimalне смешиваются в одной операции.