c#NaN比较Equals()和==之间的差异

看一下这个 :

var a = Double.NaN; Console.WriteLine(a == a); Console.ReadKey(); 

打印“假”

  var a = Double.NaN; Console.WriteLine(a.Equals(a)); Console.ReadKey(); 

打印“真实”!

为什么打印“真实”? 由于浮点数规范,NaN的值不等于它自己! 所以似乎Equals()方法实现错误……我错过了什么?

我发现了一篇解决您问题的文章: .NET安全博客:为什么==和等于方法返回浮点值的不同结果

根据IEC 60559:1989,具有NaN值的两个浮点数永远不相等。 但是,根据System.Object :: Equals方法的规范,需要重写此方法以提供值相等语义。 […]

所以现在我们对Equals应该意味着什么有两个相互矛盾的想法。 Object :: Equals表示BCL值类型应覆盖以提供值相等,而IEC 60559表示NaN不等于NaN。 ECMA规范的分区I通过在8.2.5.2节[下面]中记录这个特定情况来解决这个冲突。


更新: CLI规范(ECMA-335)第8.2.5节的全文对此有了更多的了解。 我在这里复制了相关的部分:

8.2.5价值的同一性和平等性

在所有值对上定义了两个二元运算符: identityequality 。 它们返回一个布尔结果,是数学等价运算符 ; 也就是说,它们是:

  • 反身 – a op a是真的。
  • 对称 – 当且仅当b op a为真时, a op b才为真。
  • 传递 – 如果a op b为真且b op c为真,则a op c为真。

此外,虽然身份总是意味着平等,但事实并非如此。 […]

8.2.5.1身份

身份运营商由CTS定义如下。

  • 如果值具有不同的确切类型,则它们不相同。
  • 否则,如果它们的确切类型是值类型,那么它们是相同的,当且仅当值的位序列是相同的时,才是逐位的。
  • 否则,如果它们的确切类型是引用类型,那么它们是相同的,当且仅当值的位置相同时。

Identity通过ReferenceEquals方法在System.Object上实现。

8.2.5.2平等

对于值类型,等于运算符是精确类型定义的一部分。 等式的定义应遵守以下规则:

  • 如上所述,平等应该是等价运算符。
  • 如前所述,身份应该意味着平等。
  • 如果其中一个(或两个)操作数是一个盒装值,[…]

通过Equals方法在System.Object上实现Equality。

[ 注意 :虽然IEC 60559:1989定义了两个浮点NaN以始终比较为不等,但System.Object.Equals的合同要求覆盖必须满足等价运算符的要求。 因此, System.Double.EqualsSystem.Single.Equals在比较两个NaN时返回True,而在这种情况下,相等运算符返回False,符合IEC标准的要求。 结束说明 ]

上面没有指定==运算符的属性(最终注释除外); 它主要定义ReferenceEqualsEquals的行为。 对于==运算符的行为, C#语言规范(ECMA-334) (第14.9.2节)明确了如何处理NaN值:

如果操作数[to operator == ]是NaN,则结果为false

Equals哈希表之类的东西。 因此它的合同要求a.Equals(a)

MSDN声明:

对于Equals方法的所有实现,以下语句必须为true。 在列表中,x,y和z表示非空的对象引用。

x.Equals(x)返回true,但涉及浮点类型的情况除外。 参见IEC 60559:1989,微处理器系统的二进制浮点算法。

x.Equals(y)返回与y.Equals(x)相同的值。

如果x和y都是NaN,则x.Equals(y)返回true。

如果(x.Equals(y)&& y.Equals(z))返回true,则x.Equals(z)返回true。

只要未修改x和y引用的对象,对x.Equals(y)的连续调用将返回相同的值。

x.Equals(null)返回false。

有关Equals方法的其他必需行为,请参阅GetHashCode。

我发现奇怪的是它声明“x.Equals(x)返回true,除了涉及浮点类型的情况。参见IEC 60559:1989,微处理器系统的二进制浮点运算。” 但同时要求NaN等于NaN。 那他们为什么要把这个例外放进去呢? 因为不同的NaNs?

以类似的方式使用IComparer ,也必须违反浮点标准。 由于IComparer需要一致的总排序。

如果我冒险猜测,可能是支持在字典中使用double值作为键。

如果x.Equals(y)x = double.NaNy = double.NaN返回false ,那么你可以得到如下代码:

 var dict = new Dictionary(); 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)); 

基本上,对于某个值永远不会返回trueEquals实现,您将拥有一个能够为键提供以下奇怪行为的类型:

  • 可以无限次地添加到字典中
  • 无法使用ContainsKey检测到,因此……
  • 永远不能使用Remove

请记住,由于这个原因, EqualsGetHashCode密切相关(C#编译器甚至会警告你,如果你已经覆盖了一个而没有另一个) – 为什么他们首先出现的很大一部分是为了方便使用类型作为哈希表键。

就像我说的那样,这只是猜测。

虽然你是正确的NaN == NaN是假的, double.EqualsNaN.Equals(NaN)为真的方式特别处理NaN 。 这是reflection器方法的.NET 4实现:

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

有关何时使用==Equals更多信息,请查看此链接 。 由杰出的领导人Jon Skeet撰写。