Недавно я снова поигрался с типами данных 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?
Двойной результат неверен.
@HansPassant нет, это правильно: рабочий пример.
Не думайте о десятичной дроби как об универсальной замене двойной. Оба имеют разные ограничения, когда их следует использовать или нет. Если у вас есть некоторый контроль над диапазоном десятичных знаков, ваша система должна работать в пределах десятичных знаков. Если не так, как здесь, используйте двойное число или переставьте математические выражения таким образом, чтобы оставаться в пределах десятичных знаков десятичного типа, а затем используйте десятичное число.
decimal сохраняет десятичное представление числа с определенной точностью. В этом случае для правильного вывода десятичного числа требовалось явно спросить, до скольких цифр точности вы хотите округлить число доMath.Round(result, 2) => 1.5Комментарий по округлению, хотя и сбивает с толку на первый взгляд, является своего рода отвлекающим маневром. Вы выполняете вычисления, точный результат которых находится на «границе» округления. Любые вычисления с плавающей запятой или десятичные числа могут привести к крошечным ошибкам округления, поэтому вы никогда не можете гарантировать, на какую «сторону» границы попадет вычисленный результат. Чтобы безопасно округлить, вы можете округлить дважды: один раз, чтобы удалить крошечные ошибки округления (оставив ровно 1,5), а затем еще раз округлить до ближайшего целого числа. Это гарантирует, что все, что находится достаточно близко к границе, будет двигаться в одном направлении.
В текущей структуре вы можете посмотреть, (1m/3m).Scale сколько десятичных знаков в ней будет. Вы увидите, что это не может быть точным.
И десятичное число, и число с плавающей запятой являются числами с плавающей запятой, хотя одно использует 128 бит, а другое — 64. Реальная ошибка округления возникает при 1/3 и видна только в том случае, если вы отображаете 28 цифр или более. В противном случае оба числа отображаются как 1.5. Из 28 цифр 1.0/3.0 равен 0,3333333333333333148296162562, а 1m/3m равен 0,3333333333333333333333333333.
Короче говоря, правильно decimal, а не double.. Или, скорее, decimal имеет меньшую ошибку, чем double.
@Ralf: Спасибо за совет относительно правильного выбора типов данных в зависимости от конкретной ситуации. Вообще, я довольно неохотно использую decimal. Большинство моих обычных случаев использования оправдывают использование double вместо decimal.
@NeilT (и @Ralf): Я понимаю совет по выполнению округления в два этапа. Однако я стараюсь быть с этим очень осторожным, поскольку в других ситуациях это может фактически увеличить ошибку округления. Например, я обычно не хочу округлять 1,45 до 1,5, а затем 1,5 до 2.
@PanagiotisKanavos: Большое спасибо за эту информацию. Очень проницательно. Я предполагаю, что данное представление 1.0/3.0 возникает из-за преобразования значения double в значение decimal, верно? С точностью до 64-битного типа double все десятичные дроби равны трем. Я предполагаю, что четверное число с плавающей запятой на самом деле будет включать в себя больше троек после десятичного знака.
@BartHofland - re «Обычно я не хочу округлять 1,45 до 1,5, а затем от 1,5 до 2», способ решения этой проблемы — выполнить первое округление с высоким уровнем точности, например до 10dp или аналогичного. Тогда 1.49999999999874 и 1.500000000000295 округляются до 1.5000000000. Затем вы можете безопасно округлить до 2dp (или до любого другого значения), чтобы оба значения стали 1.50 (а 1.4500.... станет 1.45). Конечно, даже первое округление будет иметь свои границы, и этого не избежать, но для реалистичных расчетов крайне маловероятно, что эти границы будут актуальны.





Потому что 1/3 десятичной дроби равна 0,33333333333333333333333333333.
0,33333333333333333333333333333 * 4,5 = 1,499999999999999999999999999998
Если переставить вот так:
(1.0 * 4.5) / 3
тогда вы получите ответ 1,5. Аналогично, если вы исключите лишний 1.0, вы получите 1,5.
О да. Конечно. Я понимаю. Не может быть никакой гарантии, что не возникнут ошибки округления при делении. Поэтому всегда важно помнить, что независимо от типа данных могут возникать ошибки округления деления и как с этими ошибками можно справиться.
Во втором абзаце вы написали: «... десятичное число на самом деле лучше, чем двойное, когда дело доходит до ошибок округления...». Это неправда. Десятичный тип не всегда «исправляет» ошибки округления.
Представьте, что у вас есть двое братьев и сестер, и вы трое унаследовали 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) всегда существует вероятность того, что определенные бесконечные повторения группы цифр после десятичного знака будут усечены в определенной точке, что приведет к ошибкам округления.
1.0 / 3.0не может быть правильно представлено в десятичной системе счисления (по основанию 10).