Не «двойная» проблема, а «десятичная» проблема

Недавно я снова поигрался с типами данных double и decimal в C#. Насколько я понял, decimal следует использовать в тех случаях, когда следует избегать ошибок десятичного округления, например, в финансовых расчетах.

Поэтому я искал демонстрационный пример, показывающий, что decimal на самом деле лучше, чем double, когда дело касается ошибок округления. Я придумал такой расчет: разделите 1 на 3 и умножьте результат на 4,5. Конечный результат должен быть 1,5.

Поэтому я выполнил следующий код C# в консольном приложении, чтобы сравнить decimal и double:

Console.WriteLine($"Double result: {1.0 / 3.0 * 4.5}");
Console.WriteLine($"Decimal result: {1m / 3m * 4.5m}");

Результат меня сильно поразил!:

Double result: 1.5
Decimal result: 1.4999999999999999999999999998

При округлении до целых значений двойной результат округляется до 2, а десятичный результат округляется до 1...

Почему здесь неправильно использовать тип decimal?

1.0 / 3.0 не может быть правильно представлено в десятичной системе счисления (по основанию 10).
wohlstad 26.06.2024 09:42

Двойной результат неверен.

Hans Passant 26.06.2024 09:44

@HansPassant нет, это правильно: рабочий пример.

MakePeaceGreatAgain 26.06.2024 09:45

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

Ralf 26.06.2024 09:54
decimal сохраняет десятичное представление числа с определенной точностью. В этом случае для правильного вывода десятичного числа требовалось явно спросить, до скольких цифр точности вы хотите округлить число доMath.Round(result, 2) => 1.5
Diego D 26.06.2024 09:56

Комментарий по округлению, хотя и сбивает с толку на первый взгляд, является своего рода отвлекающим маневром. Вы выполняете вычисления, точный результат которых находится на «границе» округления. Любые вычисления с плавающей запятой или десятичные числа могут привести к крошечным ошибкам округления, поэтому вы никогда не можете гарантировать, на какую «сторону» границы попадет вычисленный результат. Чтобы безопасно округлить, вы можете округлить дважды: один раз, чтобы удалить крошечные ошибки округления (оставив ровно 1,5), а затем еще раз округлить до ближайшего целого числа. Это гарантирует, что все, что находится достаточно близко к границе, будет двигаться в одном направлении.

Neil T 26.06.2024 09:58

В текущей структуре вы можете посмотреть, (1m/3m).Scale сколько десятичных знаков в ней будет. Вы увидите, что это не может быть точным.

Ralf 26.06.2024 09:58

И десятичное число, и число с плавающей запятой являются числами с плавающей запятой, хотя одно использует 128 бит, а другое — 64. Реальная ошибка округления возникает при 1/3 и видна только в том случае, если вы отображаете 28 цифр или более. В противном случае оба числа отображаются как 1.5. Из 28 цифр 1.0/3.0 равен 0,3333333333333333148296162562, а 1m/3m равен 0,3333333333333333333333333333.

Panagiotis Kanavos 26.06.2024 10:00

Короче говоря, правильно decimal, а не double.. Или, скорее, decimal имеет меньшую ошибку, чем double.

Panagiotis Kanavos 26.06.2024 10:04

@Ralf: Спасибо за совет относительно правильного выбора типов данных в зависимости от конкретной ситуации. Вообще, я довольно неохотно использую decimal. Большинство моих обычных случаев использования оправдывают использование double вместо decimal.

Bart Hofland 26.06.2024 10:29

@NeilT (и @Ralf): Я понимаю совет по выполнению округления в два этапа. Однако я стараюсь быть с этим очень осторожным, поскольку в других ситуациях это может фактически увеличить ошибку округления. Например, я обычно не хочу округлять 1,45 до 1,5, а затем 1,5 до 2.

Bart Hofland 26.06.2024 10:33

@PanagiotisKanavos: Большое спасибо за эту информацию. Очень проницательно. Я предполагаю, что данное представление 1.0/3.0 возникает из-за преобразования значения double в значение decimal, верно? С точностью до 64-битного типа double все десятичные дроби равны трем. Я предполагаю, что четверное число с плавающей запятой на самом деле будет включать в себя больше троек после десятичного знака.

Bart Hofland 26.06.2024 10:41

@BartHofland - re «Обычно я не хочу округлять 1,45 до 1,5, а затем от 1,5 до 2», способ решения этой проблемы — выполнить первое округление с высоким уровнем точности, например до 10dp или аналогичного. Тогда 1.49999999999874 и 1.500000000000295 округляются до 1.5000000000. Затем вы можете безопасно округлить до 2dp (или до любого другого значения), чтобы оба значения стали 1.501.4500.... станет 1.45). Конечно, даже первое округление будет иметь свои границы, и этого не избежать, но для реалистичных расчетов крайне маловероятно, что эти границы будут актуальны.

Neil T 26.06.2024 11:27
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
13
93
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Потому что 1/3 десятичной дроби равна 0,33333333333333333333333333333.

0,33333333333333333333333333333 * 4,5 = 1,499999999999999999999999999998

Если переставить вот так:

(1.0 * 4.5) / 3

тогда вы получите ответ 1,5. Аналогично, если вы исключите лишний 1.0, вы получите 1,5.

О да. Конечно. Я понимаю. Не может быть никакой гарантии, что не возникнут ошибки округления при делении. Поэтому всегда важно помнить, что независимо от типа данных могут возникать ошибки округления деления и как с этими ошибками можно справиться.

Bart Hofland 26.06.2024 10:06

Во втором абзаце вы написали: «... десятичное число на самом деле лучше, чем двойное, когда дело доходит до ошибок округления...». Это неправда. Десятичный тип не всегда «исправляет» ошибки округления.

Представьте, что у вас есть двое братьев и сестер, и вы трое унаследовали 10 000 евро в равных долях. Сколько каждый из вас получит? 3333,33 евро. Остался один цент. Кто это получит? Эта проблема также существует в десятичном типе. Он может представлять числа только до ограниченного количества десятичных знаков (называемого масштабом).

В C# при делении 1m/3m результат сохраняется с использованием определенного фиксированного количества десятичных знаков. Это не является и не может быть точным результатом. Другие языки, такие как Java, не позволяют выполнять десятичное деление 1/3 без указания масштаба результата, и это хорошо, поскольку позволяет осознать проблему точности.

Десятичный тип полезен для финансовых расчетов, поскольку он хранит числа с определенным количеством десятичных знаков, точно так же, как банки хранят баланс вашего счета и суммы транзакций или как магазины обращаются со скидками. Например, если вы пойдете в магазин, где действует распродажа со скидкой 1/3 на все, и купите что-то за 10 евро, вы заплатите либо 6,66 евро, либо 6,67 евро, в зависимости от его условий, но это будет конечно, не будет 6,6666666 евро.... Десятичный тип будет представлять либо 6,66 евро, либо 6,67 евро с точной точностью.

Да. Спасибо. Ты прав. Я действительно слишком наивно упустил из виду поведение округления делений. Независимо от внутреннего представления (двоичное в случае double или десятичное в случае decimal) всегда существует вероятность того, что определенные бесконечные повторения группы цифр после десятичного знака будут усечены в определенной точке, что приведет к ошибкам округления.

Bart Hofland 26.06.2024 10:13

Другие вопросы по теме