17

看一下这个 :

    var a = Double.NaN;

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

打印“假”

    var a = Double.NaN;

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

打印“真”!

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

4

4 回答 4

14

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

根据 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 价值观的同一性和平等性

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

  • 反身——a op a是真的。
  • 对称——a op b当且仅当b op a为真时为真。
  • 传递性——如果a op b是真的并且b op c是真的,那么a op c是真的。

此外,虽然同一性总是意味着平等,但反之则不然。[...]

8.2.5.1 身份

身份运算符由 CTS 定义如下。

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

身份是System.Object通过该ReferenceEquals方法实现的。

8.2.5.2 平等

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

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

平等是 System.Object通过该Equals 方法实现的。

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

上面完全没有指定==操作符的属性(最后的注释除外);它主要是定义 和 的ReferenceEquals行为Equals。对于==运算符的行为,C# 语言规范 (ECMA-334)(第 14.9.2 节)清楚地说明了如何处理 NaN 值:

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

于 2011-02-08T14:35:05.713 回答
9

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

MSDN 状态:

对于 Equals 方法的所有实现,以下语句必须为真。在列表中,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。那么他们为什么要放这个例外呢?因为不同的NaN?

以类似的方式使用 IComparer<double>浮点标准也必须被违反。因为IComparer需要一致的总排序。

于 2011-02-08T14:46:13.863 回答
6

如果我冒昧地猜测一下,这可能是为了支持将double值用作字典中的键。

如果x.Equals(y)返回and false,那么你可以有这样的代码:x = double.NaNy = 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(C# 编译器甚至会警告您,如果您在没有另一个的情况下覆盖了其中一个)——它们之所以存在的很大一部分原因是为了便于将类型用作哈希表键。

就像我说的,这只是一个猜测。

于 2011-02-08T14:31:34.520 回答
3

虽然您是正确的,但这NaN == NaN是错误的,但在某种程度上是正确的,double.Equals特别是不同的处理方式。这是反射器方法的 .NET 4 实现:NaNNaN.Equals(NaN)

public bool Equals(double obj)
{
    return ((obj == this) || (IsNaN(obj) && IsNaN(this)));
}
于 2011-02-08T13:46:04.447 回答