С# Math дает неправильные результаты!

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

double Value = 141.1;
double Discount = 25.0;
double disc = Value * Discount / 100; // disc = 35.275
Value -= disc; // Value = 105.824999999999999

Value = Functions.Round(Value, 2); // Value = 105.82

Я использую удвоения для представления довольно небольших чисел. Каким-то образом при вычислении 141,1 - 35,275 двоичное представление результата дает число, которое составляет всего 0,00000000000001. К сожалению, поскольку я затем округляю это число, это дает неправильный ответ.

Я читал об использовании Decimals вместо Doubles, но я не могу заменить каждый экземпляр Double на Decimal. Есть ли более простой способ обойти это?


person cusimar9    schedule 03.03.2011    source источник
comment
Округлить до 3 знаков после запятой, а затем округлить до 2 знаков после запятой? Ах... и имейте в виду, что Math.Round округляет с использованием округления Банкира, так что 0,5 округляется в половине случаев вверх и в половине случаев вниз. Есть возможность использовать стандартное округление. Это MidpointRounding.AwayFromZero .   -  person xanatos    schedule 03.03.2011
comment
Поскольку мы говорим здесь о валютных данных, я думаю, что вы должны провести этот рефакторинг в ближайшее время.   -  person linepogl    schedule 03.03.2011


Ответы (4)


Если вы ищете точное представление значений, которые обычно являются десятичными, вам потребуется везде заменить double на decimal. Вы просто используете неправильный тип данных. Если бы вы использовали short везде для целых чисел, а затем обнаружили, что вам нужно справляться с большими значениями, чем это поддерживается, что бы вы сделали? Это же сделка.

Тем не менее, вы действительно должны попытаться понять, что происходит для начала... почему, например, Value не равно точно 141,1.

У меня есть две статьи на эту тему:

person Jon Skeet    schedule 03.03.2011
comment
+1 За то, что у меня есть какая-то заранее написанная книга, которой можно помахать, на всякий случай. - person Albin Sunnanbo; 03.03.2011

Вы должны использовать decimal — для этого он и предназначен.

Поведение арифметики с плавающей запятой? Это именно то, что он делает. Он имеет ограниченную конечную точность. Не все числа точно представимы. На самом деле существует бесконечное число действительных чисел, и только конечное число может быть представлено. Ключевым моментом decimal для этого приложения является то, что оно использует представление с основанием 10, а double использует представление с основанием 2.

person David Heffernan    schedule 03.03.2011

Вместо того, чтобы использовать Round для округления числа, вы можете использовать некоторую функцию, которую вы пишете самостоятельно, которая использует небольшой эпсилон при округлении, чтобы учесть ошибку. Это ответ, который вы хотите.

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

person Stewart    schedule 03.03.2011
comment
Он не должен использовать binary тип с плавающей запятой. Обратите внимание, что decimal также является типом с плавающей запятой. И все же некоторые числа не могут быть точно представлены... но все десятичные числа (в пределах соответствующего диапазона и точности) могут быть представлены точно. - person Jon Skeet; 03.03.2011

Проведя большую часть утра, пытаясь заменить каждое вхождение «двойного» на «десятичное» и поняв, что я веду проигрышную битву, я еще раз взглянул на свою функцию раунда. Это может быть полезно для тех, кто не может реализовать правильное решение:

public static double Round(double dbl, int decimals) {            
        return (double)Math.Round((decimal)dbl, decimals, MidpointRounding.AwayFromZero);
    }

Сначала приведя значение к десятичному, а затем вызвав Math.Round, это вернет «правильное» значение.

person cusimar9    schedule 03.03.2011