为什么NaN不等于自身?

164
相关的IEEE标准定义了一个数字常量NaN(不是数值),并规定NaN应该与自己不相等。为什么呢?
我熟悉的所有语言都实现了这个规则。但是,它经常会引起重大问题,例如当NaN存储在容器中时出现意外行为,当NaN在正在排序的数据中时等等。更不用说,绝大多数程序员期望任何对象都等于自己(在他们了解NaN之前),因此让他们感到惊讶会增加错误和混乱。
IEEE标准经过深思熟虑,所以我确信有一个很好的理由,为什么将NaN与自身比较会出错。我只是想不出来是什么原因。
编辑:请参考此答案作为权威答案。

13
IEEE标准是由工程师设计的,而不是程序员、计算机供应商或数学库作者,这些人认为NaN规则是一场灾难。 - Jim Balter
6个回答

203
该被接受的答案完全错误,没有任何问题。不是半对或稍微有些错。我担心这个问题在搜索时出现时,会让程序员困惑和误导很长一段时间。

NaN被设计为通过所有计算来传播,像病毒一样感染它们。因此,如果在您深入复杂的计算中碰到NaN,您就不会得出一个看似合理的答案。否则,按照等同性原则,NaN/NaN应该等于1,以及所有其他后果,例如(NaN/ NaN)==1,(NaN*1)==NaN等。如果您想象一下您的计算在某个地方出错了(舍入产生了零分母,导致NaN),等等,则您可能会从您的计算中获得极其不正确(或更糟糕的是:微妙不正确)的结果,而没有明显的指示器说明为什么。

在探测数学函数的值时,还有很多很好的原因使NaN出现在计算中;链接文档中给出的一个例子是找到函数f()的零点值。在使用猜测值对函数进行探测的过程中,完全有可能会探测到一个函数f()没有合理结果的值。这允许zeros()看到NaN并继续它的工作。

NaN的替代方案是在遇到非法操作时触发异常(也称为信号或陷阱)。除了可能遇到的巨大性能惩罚之外,在当时没有保证CPU会在硬件上支持它,或者OS/语言会在软件上支持它;每个人都有自己独特的方法来处理浮点数。IEEE决定明确地将其作为NaN值在软件中处理,以便跨任何OS或编程语言进行移植。正确的浮点运算算法通常在所有浮点实现中都是正确的,无论是node.js还是COBOL(hah)。

理论上,您不必设置特定的#pragma指令、设置疯狂的编译器标志、捕获正确的异常或安装特殊的信号处理程序,就可以使看似相同的算法实际上正确地工作。不幸的是,一些语言设计师和编译器编写者一直在尽最大努力撤销这个功能。

请阅读一些IEEE 754浮点数的历史信息。此外,请参考类似问题的答案,其中委员会成员回答了:什么是所有比较结果都返回IEEE754 NaN值为假的理由? “浮点运算老人的采访” “IEEE浮点格式的历史” 每一个计算机科学家应该了解的浮点算术知识

33
我也喜欢 NaN "像病毒一样" 传播。不幸的是,它并没有这样做。例如,当你比较 NaN + 1 != 0 或者 NaN * 1 > 0 的时候,它会返回 True 或者 False ,就好像一切都很正常一样。因此,如果想要使用比较运算符,就不能指望 NaN 保护你免受问题的侵扰。既然比较无法帮助您"传播" NaN,为什么不至少让它们变得有意义呢?目前情况下,它们会破坏 NaN 在字典中的使用案例,使排序变得不稳定,等等。另外,你的回答中还有一个小错误。如果按照我的方式,NaN/NaN == 1 不会评估为 True - max
36
另外,你声称我的回答是100%完全错误的。然而,在你引用的那篇文章中,IEEE委员会的成员实际上说过:许多评论者认为保留相等关系的自反性和三歧性更有用,理由是采用NaN!= NaN似乎并没有保留任何熟悉的公理。我承认我对这个观点有些同情,所以我想重新审视一下这个答案,并提供更多背景信息。 因此,尊敬的先生,也许你可以在陈述时稍微缓和一下语气。 - max
5
我从未说过这个设计不是故意的。即使是由糟糕的逻辑或对问题理解不足所指导的故意设计,仍然是错误的。但这个讨论没有意义。你显然拥有终极真理的知识,你的工作是向像我这样的无知群众宣扬它。享受神职吧。 - max
21
在计算中传播NaN与使用NaN进行等式比较完全没有关系。对于NaN的可移植性和以位模式实现也不影响NaN是否应该与自身相等。事实上,除了底部的第一个链接答案解释了当时无法使用isnan()函数的情况是一个合理的原因之外,我在这个回答中找不到任何说明NaN != NaN的理由。然而,我看不出今天仍然有效的任何理由,除了更改语义将是一个非常糟糕的想法。 - Sven Marnach
7
@xenadu 我可以看到log(-1) == acos(2)这个等式支持当前的行为。 但是,您自己已经注意到不应该将浮点数用于相等比较,因此这种论据有些薄弱(而且有很多理由可以做出另一种决定)。但是,这不是我之前评论的重点。我的观点是上面的答案虽然正确,但没有给出任何原因来说明NaN为什么不应该与自身进行比较。您所说的一切都与这个问题完全无关。 - Sven Marnach
显示剩余12条评论

123

好的,log(-1)会返回NaNacos(2)也会返回NaN。这是否意味着log(-1) == acos(2)?显然不是这样。因此,NaN不等于本身是完全有道理的。

两年后重新审视这个问题,这里有一个“NaN安全”的比较函数:

function compare(a,b) {
    return a == b || (isNaN(a) && isNaN(b));
}

22
如果你在寻找log函数和acos函数的交点,那么所有小于-1的负值都被认为是交点。有趣的是,在实际数学中不能这样说,但是 Infinity == Infinity 为真。 - Niet the Dark Absol
9
考虑到Inf等于Inf,而且人们可能同样认为一个对象应该等于它自己,我怀疑IEEE作出这个选择背后有其他非常具体和强有力的理由。 - max
13
“1 + 3 = 4”和“2 + 2 = 4”,这是不是意味着“1 + 3 = 2 + 2”?显然是的。因此你的答案并不完全正确。 - borisdiakur
28
“log(-1) != log(-1)” 没有意义。因此,“NaN等于NaN”和“NaN不等于NaN”在所有情况下都没有意义。可以说,如果“NaN == NaN”被评估为表示未知的某个值,那么可能会更有意义,但这时候“==”操作符就不会返回一个布尔值。 - Tim Goodman
7
如果您提供两个不相等的不同数字,您的NaN安全比较函数将返回true。类似这样的代码 return a == b || (isNaN(a) && isNaN(b)) 可以解决这个问题。 - mmitchell
显示剩余27条评论

40

我的原始答案(来自4年前)在不了解做出决定的背景情况下,从现代角度批评了该决定。因此,它没有回答问题。

正确的答案在这里给出:

NaN!= NaN源于两个实用考虑:

[...]在8087算术中正式确定NaN时,没有isnan()谓词,需要为程序员提供一种方便高效的方法来检测不依赖于编程语言提供类似isnan()的NaN值,这可能需要很多年时间

这种方法有一个缺点:它使NaN在许多与数值计算无关的情况下变得不太有用。例如,当人们想要使用NaN表示缺失值并将其放入基于哈希的容器中时,他们无法这样做。

如果委员会预见到未来的使用情况,并认为它们足够重要,他们本可以选择更冗长的 !(x<x & x>x) 代替 x!=x 作为测试 NaN 的方法。然而,他们的关注点更加实用和狭窄:为数值计算提供最佳解决方案,因此他们认为自己的方法没有问题。

===

原始回答:

很抱歉,虽然我很欣赏最高票答案的思路,但我不同意它。NaN并不意味着“未定义”-请参见http://www.cs.berkeley.edu/~wkahan/ieee754status/IEEE754.PDF第7页(搜索“undefined”一词)。正如该文档所确认的那样,NaN是一个明确定义的概念。

此外,IEEE的方法是尽可能遵循常规数学规则,当他们无法遵循时,遵循“最小惊讶原则”-请参见https://dev59.com/rnI_5IYBdhLWcg3wAeLl#1573715。任何数学对象都等于自身,因此数学规则会暗示NaN == NaN应该为True。我看不到任何有效而有力的理由偏离这样一个重要的数学原则(更不用说比较三分律等不太重要的规则了)。

因此,我的结论如下。

IEEE委员会成员没有仔细思考,犯了一个错误。由于很少有人理解IEEE委员会的方法,或者关心标准对NaN的确切规定(即:大多数编译器对NaN的处理方式都违反了IEEE标准),因此没有人发出警报。因此,这个错误现在已经嵌入到标准中。由于这样的修复将破坏许多现有的代码,所以不太可能被修复。编辑:这里是一个帖子,来自一个非常有见地的讨论。注意:要获得公正的观点,您必须阅读整个主题,因为Guido与其他核心开发人员持不同的观点。然而,Guido对这个话题不太感兴趣,并且主要遵循Tim Peters的建议。如果有人拥有Tim Peters支持NaN != NaN的论据,请在评论中添加它们;它们有很大机会改变我的观点。

3
我认为,NaN 违反三分律是有道理的,但和你一样,我看不到不定义等价关系时使用 == 的合理语义理由(更进一步,如果这样的比较不能实现等价关系,我认为编程语言应明确禁止对不同类型的事物进行比较,即使存在隐式转换)。等价关系的概念在编程和数学中如此基础,违反它似乎很荒谬。 - supercat
5
如果NaN==NaN返回除true或false之外的其他结果将会有问题。但是,由于(a<b)不一定等于!(a>=b),所以我认为(a==b)不必一定等于!(a!=b)。如果让NaN==NaNNan!=NaN都返回false,则可以使需要任一定义的代码使用所需的定义。 - supercat
11
这个答案是完全错误的!请看下面我的答案。 - russbishop
3
我不知道有任何公理或假设声称一个数学对象(你甚至如何定义一个数学对象???)必须等于它本身。 - Transcendence
4
即使您基于恒等函数 f 在集合 S 上,其中 f(x)=x,我仍然认为 NaN 不是数字集合的一部分,毕竟它实质上不是一个数字。因此,我不认为从恒等函数的角度来看,NaN 应该等于自己。 - Transcendence
显示剩余9条评论

13

一个很好的特性是:如果x == x 返回false,则xNaN

(可以使用这个特性来检查 x 是否为 NaN。)


4
一个人可能拥有那个属性,但仍然使 (Nan != Nan) 返回 false。如果 IEEE 这样做了,想要测试 ab 之间等价关系的代码可以使用 !(a != b) - supercat
这是一个很好的替代 np.isnan() 和 pd.isnull() 的方法! - Statham

9

试试这个:

var a = 'asdf';
var b = null;

var intA = parseInt(a);
var intB = parseInt(b);

console.log(intA); //logs NaN
console.log(intB); //logs NaN
console.log(intA==intB);// logs false

如果 intA == intB 为真,这可能会让你得出 a==b 的结论,但它显然不是这样的。
另一种看待它的方式是 NaN 只提供了关于某个东西“不是什么”的信息,而不是它是什么。例如,如果我说“苹果不是大猩猩”和“橙子不是大猩猩”,你会得出“苹果”==“橙子”的结论吗?

8
这可能会让你认为a等于b,但这只是一个无效的结论。例如,strtol("010") == strtol("8")。 - Jim Balter
2
我不明白你的逻辑。假设 a=16777216fb=0.25c=0.125,那么 a+b == a+c 这个事实是否意味着 b==c?还是仅仅意味着这两个计算结果是无法区分的?如果没有区分它们的方法,为什么不能认为sqrt(-1)和(0.0/0.0)是无法区分的呢? - supercat
如果你的意思是说不可区分的事物应该被视为相等,我不同意这种观点。相等意味着你有一种区分两个比较对象的方法,而不仅仅是对它们缺乏相同的了解。如果你没有区分它们的方法,那么它们可能相等,也可能不相等。我可以理解NaN == NaN返回'undefined',但不能是true。 - Mike C
1
@MikeC 已经非常准确地解释了原因,而且没有太多的语法错误。 - Ody
这么多答案,我只能理解你所解释的部分,赞! - Kushal
嗯,这很有趣。这似乎是个权衡:为了保持传递性而破坏 == 的自反性。 - Alexander

2
实际上,数学中有一个被称为“单元”值的概念。这些值是精心构建的扩展,用于解决系统中的异常问题。例如,在复平面中,你可以将无穷远处的环看作是一个点或一组点,从而消除一些以前令人烦恼的问题。在集合的基数方面还有其他的例子,你可以证明只要 |P(A)| > |A|,就可以选择连续无限个不同的结构,而且不会出现任何问题。
免责声明:我只是根据我的模糊记忆描述了一些有趣的注意事项。如果我在上述引用的概念的表达方面做得很糟糕,我深感抱歉。
如果你认为NaN是一个孤立的值,那么你可能对一些结果感到不满,比如等号运算符的工作方式不符合你的期望/要求。然而,如果你选择相信NaN是由一个孤立的占位符表示的“恶劣”连续体,那么你对等号运算符的行为非常满意。换句话说,你失去了你在大海捕捞的鱼,但你又抓住了一条外表相同但同样臭的鱼。

1
在数学中,您可以添加无限大和类似的值。但是,它们永远不会打破等价关系。程序员的相等性表示数学中的等价关系,这是根据定义具有自反性。一个糟糕的程序员可能会定义不具备自反性、对称性和传递性的“==”;不幸的是,Python 不会阻止他。但当 Python 本身使“==”不具备自反性,并且您甚至无法覆盖它时,从实用视角(容器成员资格)和优雅/思维清晰度视角来看,这是一场完全的灾难。 - max

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接