Безопасно ли проверять значения с плавающей запятой на равенство 0?
-
20-08-2019 - |
Вопрос
Я знаю, что обычно вы не можете полагаться на равенство между значениями двойного или десятичного типа, но мне интересно, является ли 0 особым случаем.
Хотя я могу понять неточности между 0.00000000000001 и 0.00000000000002, 0 само по себе кажется довольно сложным перепутать, поскольку это просто ничто.Если вы ни в чем не уверены, это уже не ничто.
Но я мало что знаю об этой теме, так что не мне об этом говорить.
double x = 0.0;
return (x == 0.0) ? true : false;
Всегда ли это будет возвращать true?
Решение
Это так безопасный ожидать, что сравнение вернет true
тогда и только тогда, когда переменная double имеет значение ровно 0.0
(что в вашем исходном фрагменте кода, конечно, так и есть).Это согласуется с семантикой ==
оператор. a == b
означает "a
равно b
".
Это так небезопасно (потому что это не правильно) ожидать, что результат некоторого вычисления будет равен нулю в арифметике с двойным (или, в более общем смысле, с плавающей запятой) значением всякий раз, когда результат того же вычисления в чистой математике равен нулю.Это происходит потому, что, когда начинаются вычисления, появляется ошибка точности с плавающей запятой - понятие, которого не существует в математической арифметике действительных чисел.
Другие советы
Если вам нужно выполнить много сравнений "на равенство", возможно, было бы неплохо написать небольшую вспомогательную функцию или метод расширения в .NET 3.5 для сравнения:
public static bool AlmostEquals(this double double1, double double2, double precision)
{
return (Math.Abs(double1 - double2) <= precision);
}
Это можно было бы использовать следующим образом:
double d1 = 10.0 * .1;
bool equals = d1.AlmostEquals(0.0, 0.0000001);
Для вашего простого образца этот тест подходит.Но как насчет этого:
bool b = ( 10.0 * .1 - 1.0 == 0.0 );
Помните, что .1 является повторяющимся десятичным числом в двоичном формате и не может быть точно представлено.Затем сравните это с этим кодом:
double d1 = 10.0 * .1; // make sure the compiler hasn't optimized the .1 issue away
bool b = ( d1 - 1.0 == 0.0 );
Я оставлю вас, чтобы вы провели тест и увидели реальные результаты:таким образом, вы, скорее всего, запомните это.
Из записи MSDN для Удваивается.Равно:
Точность в сравнениях
Метод Equals следует использовать с осторожностью, потому что два явно эквивалентных значения могут быть неравными из-за разной точности двух значений.В следующем примере сообщается что значение Double .3333 и Значение Double, возвращаемое путем деления 1 на 3, неравны.
...
Вместо сравнения на предмет равенства один из рекомендуемых методов включает определение допустимого предела разницы между двумя значениями (например, 0,01% от одного из значений).Если абсолютное значение разницы между двумя значениями меньше или равно этому пределу, разница вероятно, это связано с различиями в точности и, следовательно, значениях вероятно, будут равны.Следующий пример использует этот метод для сравнения .33333 и 1/3, двух двойных значений которые в предыдущем примере кода были найдены неравными.
Кроме того, смотрите Двойной.Эпсилон.
Проблема возникает, когда вы сравниваете различные типы реализации значений с плавающей запятой, напримерсравнение float с double.Но с тем же типом это не должно быть проблемой.
float f = 0.1F;
bool b1 = (f == 0.1); //returns false
bool b2 = (f == 0.1F); //returns true
Проблема в том, что программист иногда забывает, что для сравнения выполняется неявное приведение типа (double к float), и это приводит к ошибке.
Если число было непосредственно присвоено значению с плавающей точкой или double, то можно безопасно протестировать его на ноль или любое целое число, которое может быть представлено в 53 битах для double или 24 битах для float.
Или, другими словами, вы всегда можете присвоить целочисленное значение double, а затем сравнить double обратно с тем же целым числом и быть гарантированным, что оно будет равным.
Вы также можете начать с присвоения целого числа и продолжить работу с простыми сравнениями, придерживаясь сложения, вычитания или умножения на целые числа (предполагая, что результат меньше 24 бит для числа с плавающей точкой и 53 бит для double).Таким образом, вы можете обрабатывать числа с плавающей запятой и удвоения как целые числа при определенных контролируемых условиях.
Нет, это не нормально.Так называемые денормализованные значения (субнормальные), при сравнении равные 0.0, будут сравниваться как false (ненулевые), но при использовании в уравнении будут нормализованы (станут 0.0).Таким образом, использовать это как механизм, позволяющий избежать деления на ноль, небезопасно.Вместо этого добавьте 1.0 и сравните с 1.0.Это гарантирует, что все субнормальные значения будут рассматриваться как нулевые.
Попробуйте это, и вы обнаружите, что == ненадежно для double / float .
double d = 0.1 + 0.2;
bool b = d == 0.3;
Вот этот ответ от Quora.
На самом деле, я думаю, что лучше использовать следующие коды для сравнения двойного значения с равным 0.0:
double x = 0.0;
return (Math.Abs(x) < double.Epsilon) ? true : false;
То же самое для float:
float x = 0.0f;
return (Math.Abs(x) < float.Epsilon) ? true : false;