С# NaN сравнительные различия между Equals() и ==

Проверьте это:

    var a = Double.NaN;

    Console.WriteLine(a == a);
    Console.ReadKey();

Печать "False"

    var a = Double.NaN;

    Console.WriteLine(a.Equals(a));
    Console.ReadKey();

Печатает "True"!

Почему он печатает "True"? Из-за спецификации чисел с плавающей запятой значение NaN не равно самому себе! Таким образом, кажется, что метод Equals() реализован неправильно... Я что-то пропустил?

Ответ 1

Я нашел статью, посвящённую вашему вопросу: .NET Security Blog: Почему == и метод Equals возвращают разные результаты для значений с плавающей точкой

Согласно МЭК 60559: 1989, два числа с плавающей запятой со значениями NaN никогда не равны. Однако, в соответствии со спецификацией System.Object:: Equals метод, это желательно переопределить этот метод для предоставить семантику равенства ценности. [...]

Итак, теперь у нас есть две противоречивые идеи о том, что должно означать Равенство. Object:: Equals говорит, что значение BCL типы должны переопределяться для обеспечения ценности равенство, а МЭК 60559 говорит, что NaN не равен NaN. Раздел я спецификация ECMA обеспечивает разрешение для этот конфликт, сделав заметку о этот конкретный случай в разделе 8.2.5.2 [ниже]


Обновление:. Полный текст раздела 8.2.5 из спецификации CLI (ECMA-335) проливает некоторые больше света на этом. Я скопировал соответствующие биты здесь:

8.2.5 Идентичность и равенство значений

Существует два бинарных оператора по всем парам значений: тождество и равенство. Они возвращают логический результат и являются математическими операторов эквивалентности; то есть они:

  • Рефлексивный - a op a является истинным.
  • Симметричный - a op b является истинным тогда и только тогда, когда b op a истинно.
  • Transitive - если a op b истинно, а b op c - true, тогда a op cправда.

Кроме того, хотя идентификация всегда означает равенство, обратное не правда. [...]

8.2.5.1 Идентификация

Идентичный оператор определяется CTS следующим образом.

  • Если значения имеют разные точные типы, то они не идентичны.
  • В противном случае, если их точный тип - тип значения, то они идентичны, если и только если битовые последовательности значения по-разному совпадают.
  • В противном случае, если их точный тип является ссылочным типом, то они идентичны тогда и только тогда, когда местоположения значений одинаковы.

Идентичность реализуется на System.Object с помощью метода ReferenceEquals.

8.2.5.2 Равенство

Для типов значений оператор равенства является частью определения точного тип. Определения равенства должны соблюдать следующие правила:

  • Равенство должно быть оператором эквивалентности, как определено выше.
  • Идентичность должна подразумевать равенство, как указано ранее.
  • Если операнд (или оба) является коротким значением, [...]

Равенство реализуется на System.Object через EqualsМетод.

[Примечание: хотя две с плавающей запятой NaN определены в МЭК 60559: 1989 всегда сравниваются как неравные, контракт для System.Object.Equals требует, чтобы переопределения удовлетворяли требования к эквивалентности оператор. Следовательно, System.Double.Equals и System.Single.Equals return True при сравнении двух NaN, тогда как оператор равенства возвращает False в в этом случае, как того требует МЭК стандарт. end note]

Приведенное выше не указывает свойства оператора == вообще (кроме последней заметки); это прежде всего определяет поведение ReferenceEquals и Equals. Для поведения оператора == спецификация языка С# (ECMA-334) (раздел 14.9.2) ясно о том, как лечить Значения NaN:

Если любой из операндов [до operator ==] равен NaN, результат будет false

Ответ 2

Equals выполняется для вещей, таких как hashtables. Таким образом, контракт требует, чтобы a.Equals(a).

Состояние MSDN:

Следующие утверждения должны быть верны для всех реализаций метода Equals. В списке x, y и z представляют ссылки на объекты, которые не являются нулевыми.

x.Equals(x) возвращает true, за исключением случаев, связанных с типами с плавающей точкой. См. IEC 60559: 1989, Бинарная арифметика с плавающей точкой для микропроцессорных систем.

x.Equals(y) возвращает то же значение, что и y.Equals(x).

x.Equals(y) возвращает true, если оба x и y являются NaN.

Если (x.Equals(y) & y.Equals(z)) возвращает true, то x.Equals(z) возвращает true.

Последовательные вызовы x.Equals(y) возвращают одно и то же значение, если объекты, на которые ссылаются x и y, не изменяются.

x.Equals(null) возвращает false.

См. GetHashCode для дополнительных требуемых действий, относящихся к методу Equals.

То, что я нахожу странным, заключается в том, что он утверждает, что "x.Equals(x) возвращает true, за исключением случаев, когда речь идет о типах с плавающей точкой. См. IEC 60559: 1989, Бинарная арифметика с плавающей точкой для микропроцессорных систем". но в то же время требует, чтобы NaN равнялось NaN. Так почему они ввели это исключение? Из-за разных NaN?

Аналогичным образом при использовании IComparer<double> также должен быть нарушен стандарт с плавающей запятой. Поскольку IComparer требует согласованного полного упорядочения.

Ответ 3

Если бы я рискнул предположить, возможно, это связано с поддержкой использования double значений в качестве ключей в словаре.

Если x.Equals(y) вернул false для x = double.NaN и y = double.NaN, то вы могли бы иметь такой код:

var dict = new Dictionary<double, string>();

double x = double.NaN;

dict.Add(x, "These");
dict.Add(x, "have");
dict.Add(x, "duplicate");
dict.Add(x, "keys!");

Я думаю, что большинство разработчиков сочли бы это поведение довольно не интуитивным. Но еще более нелогичным было бы это:

// This would output false!
Console.WriteLine(dict.ContainsKey(x));

По сути, с реализацией Equals которая никогда не возвращает true для определенного значения, у вас будет тип, способный предоставлять ключи со следующим странным поведением:

  • Может быть добавлено в словарь неограниченное количество раз
  • Не удалось обнаружить с помощью ContainsKey, и поэтому...
  • Никогда не может быть удален с помощью Remove

Помните, что Equals очень тесно связан с GetHashCode по этой причине (компилятор С# даже предупреждает вас, если вы переопределяете один без другого) - большая часть того, почему они существуют, заключается в том, чтобы облегчить использование типы как ключи хеш-таблицы.

Как я уже сказал, это всего лишь предположение.

Ответ 4

Пока вы правы, что NaN == NaN является ложным, double.Equals по-разному обрабатывает NaN таким образом, что NaN.Equals(NaN) является истинным. Здесь реализация .NET 4 метода из отражателя:

public bool Equals(double obj)
{
    return ((obj == this) || (IsNaN(obj) && IsNaN(this)));
}

Ответ 5

Ознакомьтесь с этой ссылкой для получения дополнительной информации о том, когда использовать == или Equals. Написано прославленным лидером Джоном Скитом.