Является ли десятичный тип dotNet уязвимым для ошибки двоичного сравнения? - PullRequest
2 голосов
/ 09 июля 2009

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

        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 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 / C #, я думаю, что это происходит в большинстве языков программирования.

Ответы [ 3 ]

6 голосов
/ 09 июля 2009

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

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

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

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

0 голосов
/ 09 июля 2009

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

от http://msdn.microsoft.com/en-us/library/system.double.epsilon.aspx:

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

0 голосов
/ 09 июля 2009

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

...