Сравнение Чисел в .NET. Окончание
Начало
+0.0 и -0.0: равны, но не идентичны
В стандарте IEEE 754 используется знаковый ноль.
+0.0 и -0.0 сравниваются как равные, поэтому == и Equals возвращают true:double pz = +0.0;
double nz = -0.0;
Console.WriteLine(pz == nz); // True
Console.WriteLine(pz.Equals(nz)); // True
Однако знак всё же имеет значение для некоторых операций:
Console.WriteLine(1.0 / +0.0); // +Infinity
Console.WriteLine(1.0 / -0.0); // -Infinity
Таким образом, «равенство» не всегда означает «взаимозаменяемость в каждом выражении».
Примечание: значения NaN также имеют знак. Такие методы, как
float.IsNegative, возвращают true для -NaN.CompareTo обеспечивает упорядочивание, включая обработку NaN
Операторы сравнения с NaN намеренно неудобны, поскольку NaN не упорядочен. Для сортировки .NET предоставляет возможность упорядочивания через CompareTo.
Для Half, double и float:
- NaN равно NaN;
- NaN меньше, чем не-NaN.
Это поведение явно закодировано в реализациях CompareTo в исходном коде среды выполнения.
Хэши нормализованы для NaN и знакового нуля
Еще один тонкий, но важный момент: GetHashCode() намеренно канонизирует значения. В методах
Half.GetHashCode, Double.GetHashCode и Single.GetHashCode .NET гарантирует, что:- все шаблоны битов NaN дают одинаковый хэш;
- +0.0 и -0.0 дают одинаковый хэш.
Это необходимо для согласованности с
Equals, когда значения используются в качестве ключей.Особый случай точности: значения, которые кажутся равными, могут быть не равными
Отдельным источником путаницы является представление. Многие десятичные значения не могут быть точно выражены в двоичном представлении с плавающей запятой, поэтому прямое сравнение на равенство часто не работает:
Console.WriteLine(0.1 + 0.2 == 0.3); // False
Тип
decimal решает эту проблему, поскольку хранит целое число в десятичной системе счисления, а не дробь в двоичной системе. Такие значения, как 0.1m, 0.2m и 0.3m, точно воспроизводимы, поэтому:Console.WriteLine(0.1m + 0.2m == 0.3m); // True
Точность decimal составляет от 28 до 29 значащих цифр. Внутри используется 96-битное целое число плюс коэффициент масштабирования (от 0 до 28 десятичных знаков) и знак.
Важные ограничения:
- decimal не является числом с произвольной точностью. Оно по-прежнему имеет конечный диапазон и может переполняться;
- Операции, которые дают более 28–29 значащих цифр, округляются;
- decimal относится к точному десятичному представлению, а не к поведению чисел с плавающей запятой по стандарту IEEE. В нём нет значений NaN или Infinity.
Для числовых алгоритмов сравнивайте с допуском (абсолютным + относительным), а не с
Double.Epsilon:static bool NearlyEqual(
double a,
double b,
double relTol = 1e-12,
double absTol = 1e-15)
{
if (a == b)
return true; // нули
if (double.IsNaN(a) || double.IsNaN(b))
return false;
if (double.IsInfinity(a) || double.IsInfinity(b))
return false;
double diff = Math.Abs(a - b);
double scale = Math.Max(Math.Abs(a), Math.Abs(b));
return diff <= Math.Max(absTol, relTol * scale);
}
Практические рекомендации
Используйте:
-
==, когда явно нужна семантика сравнения IEEE;-
Equals, когда нужна семантика равенства .NET (особенно в коллекциях);-
Half.IsNaN, double.IsNaN или float.IsNaN для проверки на NaN;- сравнение с погрешностью для вычисленных результатов с плавающей запятой;
- decimal, когда нужны точные десятичные дроби (например, деньги) и когда достаточно 28-29 значащих цифр.
Итого
И ==, и Equals являются корректными. Они отвечают на разные вопросы:
-
==: «Равны ли эти значения согласно правилам сравнения чисел с плавающей запятой IEEE?»-
Equals: «Следует ли считать эти два значения .NET равными для контрактов равенства объектов?»Как только вы разделите эти два намерения, крайние случаи, связанные с NaN, знаковым нулем и бесконечностью, станут предсказуемыми.
Источник: https://www.meziantou.net/number-comparison-in-dotnet-equals-and-ieee-754-edge-cases.htm