2009-07-09 5 views
2

Одна ошибка, я наткнуться на каждые несколько месяцев это один:Является ли dotNet десятичным типом уязвимым для двоичной ошибки сравнения?

 double x = 19.08; 
     double y = 2.01; 
     double result = 21.09; 

     if (x + y == result) 
     { 
      MessageBox.Show("x equals y"); 
     } 
     else 
     { 
      MessageBox.Show("that shouldn't happen!"); // <-- this code fires 
     } 

Вы бы предположить код для отображения «х равно у», но это не так.
Короткое объяснение состоит в том, что десятичные знаки, представленные в виде двоичной цифры, не вписываются в двойные.

Пример: 2,625 будет выглядеть так:

10,101

, потому что

1-------0-------1---------0----------1 
1 * 2 + 0 * 1 + 1 * 0.5 + 0 * 0.25 + 1 * 0,125 = 2.65 

А некоторые значения (например, в результате 19.08 плюс 2.01) не может быть представлены с битами двойной.

Одно из решений состоит в использовании константы:

 double x = 19.08; 
     double y = 2.01; 
     double result = 21.09; 
     double EPSILON = 10E-10; 

     if (x + y - result < EPSILON) 
     { 
      MessageBox.Show("x equals y"); // <-- this code fires 
     } 
     else 
     { 
      MessageBox.Show("that shouldn't happen!"); 
     } 

Если я использую десятичной вместо двойной в первом примере, результат «х равен у».
Но я спрашиваю себя, если это из-за «десятичного» типа, не уязвим для этого поведения или просто работает в этом случае, потому что значения «подходят» к 128 бит.

Возможно, у кого-то есть лучшее решение, чем использование константы?

КПП. это не проблема dotNet/C#, это происходит на большинстве языков программирования, я думаю.

+0

Это, безусловно, http://docs.sun.com/source/806-3568/ncg_goldberg.html - известная статья. – RichardOD

ответ

6

Десятичное число будет точным, если вы останетесь в пределах значений, которые являются естественными десятичными знаками в соответствующем диапазоне. Так что, если вы просто добавляете и вычитаете, например, не делая ничего, что бы исказить диапазон цифр, требуемый слишком много (добавление очень большого числа к очень маленькому числу), вы получите легко сопоставимые результаты. Умножение, вероятно, тоже будет хорошо, но я подозреваю, что легче получить неточности с ним.

Как только вы начинаете делиться, что там проблемы могут прийти - особенно если начать деление на числа, которые включают в себя другие, чем простые множители 2 или 5.

Итог: это безопасно в определенных ситуациях, но вы действительно нужно иметь хороший контроль над тем, какие операции вы будете выполнять.

Обратите внимание, что это не 128-разрядности десятичного, который помогает вам здесь - это представление чисел с плавающим в десятичных значения точек, а не плавающая двоичных значения точек. Для получения дополнительной информации см. Мои статьи о .NET binary floating point и decimal floating point.

+0

Спасибо, но проблема возникает не только с делением. Во время преобразования десятичное -> двоичное -> десятичное число может состоять в том, что десятичное число с двумя десятичными знаками преобразует в большое (даже инфинитив?) Число двоичных «десятичных» мест, которые отклоняются, если сумма больше 128 бит. Если вы преобразуете этот «округленный» двоичный код обратно в десятичный, у вас другой результат. –

+0

@SchlaWiener: Если вы конвертируете между двойным и десятичным, у вас наверняка возникнет проблема. Не делай этого. Придерживайтесь одного формата или другого. –

+0

Я не хочу конвертировать. Что касается вашего артефакта «десятичная плавающая точка», вы указываете, что десятичное число основано на «10». Означает ли это, что a + b = c всегда возвращает true, если плюс b действительно является c? –

0

System.Decimal - это всего лишь число с плавающей точкой с различным основанием, поэтому теоретически оно по-прежнему уязвимо к той ошибке, которую вы указываете. Я думаю, вы только что произошли на случай, когда округления не происходит. Дополнительная информация here.

+0

Действительно, большинство из них имеют отношение к базе 10, а десятичный тип может точно представлять эти суммы (в пределах прецизионные лимиты). Таким образом, десятичный тип идеален для использования в реальном мире, например, с деньгами. – redcalx

0

Да, структура .NET System.Double зависит от описанной проблемы.

из http://msdn.microsoft.com/en-us/library/system.double.epsilon.aspx:

два, по-видимому, эквивалентные числа с плавающей точкой может не сравнить равно из-за различий в их значащих цифр. Например, выражение C# (double) 1/3 == (double) 0.33333 не сравнивается с равным, поскольку операция деления на левой стороне имеет максимальную точность, а константа справа - точная только с указанными цифрами. Если вы создадите собственный алгоритм, определяющий, можно ли считать, что два числа с плавающей запятой считаются равными, вы должны использовать значение, большее, чем константа Epsilon, чтобы установить приемлемый абсолютный запас разницы для того, чтобы два значения считались равными. (Как правило, этот разницы во много раз больше, чем Epsilon.)