Является ли десятичный тип dotNet уязвимым для ошибки двоичного сравнения?

2

Одна ошибка, которую я натыкаюсь на каждые несколько месяцев:

        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
        }

Вы предположили бы, что код для отображения "x равен y", но это не так. Краткое объяснение состоит в том, что десятичные разряды, представленные в виде двоичной цифры, не вписываются в двойные.

Пример: 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.

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

        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!");
        }

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

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

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

Теги:
decimal
comparison

3 ответа

6
Лучший ответ

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

Как только вы начнете делиться, то, где могут возникнуть проблемы, особенно если вы начнете делиться цифрами, которые включают в себя простые коэффициенты, отличные от 2 или 5.

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

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

  • 0
    Спасибо, но проблема возникает не только с делением. Во время преобразования decimal -> binary -> decimal может случиться так, что десятичное число с двумя десятичными знаками преобразует в большое (даже бесконечное?) Количество двоичных «десятичных» разрядов, которые отбрасываются, если сумма превышает 128 бит. Если вы конвертируете этот «округленный» двоичный код обратно в десятичный, у вас будет другой результат.
  • 0
    @SchlaWiener: Если вы конвертируете между двойным и десятичным, у вас наверняка будет проблема. Не делай этого. Придерживайтесь одного формата или другого.
Показать ещё 3 комментария
0

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

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

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню