Перейти к содержимому
Теория · Операторы · урок 6 из 10

double и decimal

Целые числа C# хранит точно, пока они помещаются в тип. С дробными числами иначе, и это видно уже на сложении двух десятых.

Наберите на калькуляторе 1 ÷ 3. На экране появится 0,33333333: восемь троек, и место кончилось. Настоящая треть — бесконечная дробь, а калькулятор хранит только её начало.

Тип double устроен похоже, только хранит числа в двоичной системе. В ней число записывают всего двумя цифрами, 0 и 1. В двоичной системе даже 0.1 — бесконечная дробь, как треть в десятичной. Поэтому double хранит не саму 0.1, а ближайшее к ней число, которое помещается в тип:

C#
double sum = 0.1 + 0.2;
Console.WriteLine(sum); // 0.30000000000000004
Console.WriteLine(sum == 0.3); // False

Давайте разберём, что произошло:

  1. В 0.1 и 0.2 уже есть крошечные погрешности, и при сложении они накапливаются.
  2. Сумма печатается как 0.30000000000000004: C# показывает столько цифр, сколько нужно, чтобы отличить это число от других значений double, в том числе от 0.3.
  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 в нём хранится точно.

C#
decimal total = 0.1m + 0.2m;
Console.WriteLine(total); // 0.3
Console.WriteLine(total == 0.3m); // True

Буква m делает литерал decimal: без неё 0.1 был бы double. Чем отличаются два типа:

doubledecimal
Как хранит числодвоичными цифрамидесятичными цифрами
Значащих цифр15–1728–29
Скорость вычисленийвышениже
Для чего подходитизмерения, физика, графикаденьги и точный учёт

decimal точен только для десятичных дробей. Треть не записывается и десятичными цифрами, поэтому 1m / 3 даёт 0.3333333333333333333333333333 — дробь, округлённую до 28 знаков после точки.

Частая ошибка

Ставку налога объявили как double и умножили на неё цену типа decimal:

C#
decimal price = 19.99m;
double rate = 0.2;
decimal tax = price * rate; // ✗
Console.WriteLine(tax);
TEXT
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 не смешиваются в одной операции.