TGViewer
.NET Разработчик .NET Разработчик @netdeveloperdiary · 6.74K subscribers
Post #3270 1.69K
День 2729. #ЗаметкиНаПолях
Сравнение Чисел в .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
  • 👍 4
More from @netdeveloperdiary
  1. Sep 26, 2026День 2796. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Продолжение Начало Три…
  2. Sep 25, 2026День 2795. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Начало Проблема с позиц…
  3. Sep 24, 2026День 2794. #Оффтоп #Здоровье Сегодня будет необычный пост. Завтра в Москве стартует конфер…
  4. Sep 23, 2026День 2793. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  5. Sep 22, 2026День 2792. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  6. Sep 21, 2026🔍Тестовое собеседование с Senior C# разработчиком уже завтра 22 сентября(уже завтра!) в 1…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →