比较两个相同的 "literal" 浮点数是否相等错误?

Is comparing two same "literal" float numbers for equality wrong?

这个问题有点与语言无关,但代码是用 Java 编写的。

我们都听说比较浮点数是否相等通常是错误的。但是,如果我想比较两个完全相同的文字浮点值(或表示完全相同的文字值的字符串转换为浮点数)怎么办?

我很确定数字会完全相等(好吧,因为它们 必须 在二进制中是相等的——完全相同的东西怎么会产生两个不同的二进制数?!)但我想确定一下。

案例 1:

void test1() {
    float f1 = 4.7;
    float f2 = 4.7;
    print(f1 == f2);
}

案例二:

class Movie {
    String rating; // for some reason the type is String
}
void test2() {
    movie1.rating = "4.7";
    movie2.rating = "4.7";

    float f1 = Float.parse(movie1.rating);
    float f2 = Float.parse(movie2.rating);

    print(f1 == f2);
}

在这两种情况下,表达式 f1 == f2 的结果应该是 true。我对吗?如果 rating 具有相同的文字浮点值或字符串值,我可以安全地比较它们是否相等吗?

是的。相同的编译时间常数被一致地评估。

如果你仔细想想,它们一定是一样的,因为只有一个编译器,它确定性地将文字转换为浮点表示。

有一条经验法则,您应该将其应用于所有编程经验法则(经验法则?):

它们过于简单化,如果推得太远会导致做出愚蠢的决策。 IF 你没有完全理解经验法则背后的意图,你会搞砸的。也许经验法则仍然是完全积极的(不假思索地应用它会改善事情而不是使事情变得更糟),但它会造成损害,无论如何它不能用作辩论中的论点。

所以,考虑到这一点,很明显,问这个问题没有意义:

“鉴于经验法则 'do not use == to compare floats' 存在,它总是不好吗?”。

答案非常明显:呃,不。这并不总是很糟糕,因为根据定义,经验法则几乎永远适用,即使不是常识,也永远不会适用。

那么让我们分解一下。

为什么有一条经验法则你不应该 == 比较浮点数?

你的问题表明你已经知道了这一点:这是因为对由 IEEE754 概念(例如 java 的 doublefloat 表示的浮点数进行任何数学运算都是不精确的(与. 像 java 的 BigDecimal 这样的概念,这是准确的 *).

当遇到经验法则时,做你应该做的事情,在摸索为什么存在经验法则并意识到它不适用于你的情况时:完全忽略它。

也许您的问题可以归结为:我 认为 我理解经验法则,但也许我遗漏了什么;除了不适用于此案例的 'floating point math introduces small deviations which mess up == comparison',还有其他我不知道的经验法则的原因吗?

在这种情况下,我的回答是:据我所知,不会。

*) 但是 BigDecimal 有其自身的等式问题,例如:两个 BigDecimal 对象是否精确地表示相同的数学数字,但配置为以不同的比例呈现 'equal'?这取决于您的观点是它们是数字还是代表精确小数点的对象以及一些元属性,包括如何呈现它以及如果明确要求如何舍入。对于它的价值,BD 的 equals 实现必须做出苏菲的选择,并在 2 个同等有效的平等含义解释之间做出选择,选择 'I represent a number',而不是 'I represent a number along with a bunch of metadata'。同一个sophie的选择存在于所有JPA/Hibernate栈中:Does a JPA object represent 'a row in the database' (thus equality being defined solely by the primary key value, and if not saved yet, two objects cannot be equal, not even自身,除非相同的引用身份),或者它是否代表该行所代表的事物,例如一个学生,而不是 'a row in the DB that represents a student',在这种情况下,unid 是一个与身份无关的字段,而所有其他字段(姓名、出生日期、社会安全号码等)都有。平等很难。

是的,你可以这样比较浮点数。问题是,即使 4.7 在转换为浮点数时不是 4.7,它也会一致地转换为相同的值。

一般来说,像这样比较浮点数本身并没有错误。但是对于更复杂的数学,您可能想要使用 Math.round() 或设置一个“相同”的差异跨度,两者应该在其中才能算作“相同”。

定点数也有任意性。例如

1,000,000,001

大于

1.000,000,000

这两个数字不同吗?这取决于您需要的精度。但对于大多数用途,这些数字在功能上是相同的

This question is kind of language-agnostic…

实际上,这里没有floating-point问题,答案完全取决于语言。

不存在 floating-point 问题,因为 IEEE-754 很明确:两个 floating-point 数据(有限数、无穷大、and/or NaN)当且仅当它们对应时才比较相等到相同的实数。

存在语言问题,因为文字如何映射到 floating-point 数字以及源文本如何映射到操作因语言而异。例如,C 2018 6.4.4.2 5 表示:

All floating constants of the same source form77) shall convert to the same internal format with the same value.

脚注 77 说:

1.23, 1.230, 123e-2, 123e-02, and 1.23L are all different source forms and thus need not convert to the same internal format and value.

因此 C 标准允许 1.23 == 1.230 评估为 false。 (这是允许的历史原因,将其作为 quality-of-implementation 问题。)如果“相同”的字面浮点值表示完全相同的源文本,则此问题不会出现在 C 中;在特定的 C 实现中,完全相同的源文本必须每次都产生相同的 floating-point 值。但是,这个例子告诉我们要谨慎。

C 还允许实现在如何执行 floating-point 操作方面具有灵活性:它允许实现在计算表达式时使用比标称精度更多的精度,并且允许在同一表达式的不同部分使用不同的精度。所以 1./3. == 1./3. 可以评估为假。 某些语言,如 Python,没有良好的正式规范,并且在很大程度上对 floating-point 操作的执行方式保持沉默。可以想象 Python 实现可以使用处理器寄存器中可用的超精度将源文本 1.3 转换为 long double 或类似类型,然后将其保存为 double ,然后将源文本 1.3 转换为 long double,然后检索 double 并将其与仍在寄存器中的 long double 进行比较,并得到表示不等式的结果。

这种问题在我所知道的实现中不会发生,但是,当问这样的问题时,无论语言如何,询问规则是否始终成立,都会为可能的例外情况敞开大门。