『软件工程』计算机界 TOP 3 难题:“相等”是软件工程中许多重大问题的根源( 五 )


对于此类正常的浮点运算中不会出现的问题 , 解决方法之一就是采用联合(union)类型 。 在F#中可以这样写:
type MaybeFloat = | Float of float | Imaginary of real: float * imaginary: float | Indeterminate | /// ... 然后就可以在计算中正确处理这些情况了 。 如果在计算中遇到预料之外的NaN , 可以使用signaling NaN来抛出异常 。
Rust提供了Eq和PartialEq两个trait 。 没有实现Eq , 是==运算符不遵从反射率的一个信号 , 而Rust中的浮点类型就没有实现Eq 。 但即使不实现Eq , 你依然可以在代码中使用== 。 实现Eq可以将对象作为hash map的键使用 , 可能会导致其他地方的行为发生变化 。
但是=和浮点数还有更严重的问题 。
『软件工程』计算机界 TOP 3 难题:“相等”是软件工程中许多重大问题的根源
本文插图
常见错误:相等过于精确
我想许多开发者都熟悉IEEE-754浮点数的比较问题 , 因为绝大多数语言的“float”或“double”的实现都是IEEE-754 。 10 *(0.1) 不等于1 , 因为“0.1”实际上等于0.100000001490116119384765625 , 或0.1000000000000000055511151231257827021181583404541015625 。 如果你对此问题感到陌生 , 你可以阅读这篇文章(https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/) , 但这里的重点是 , 在浮点数上使用==进行比较是完全不安全的!你必须决定哪些数字是重要的 , 然后据此进行比较 。
(更糟糕的是 , 浮点数是许多其他类型的基础 , 如某些语言中的TDateTime类型 , 所以即使一些相等比较本该合理的地方 , 也不能正常工作 。 )
比较浮点数的正确方法是看它们是否“相近” , 而“相近”在不同语境下有不同的含义 。 这并不是简单的==能够完成的 。 如果你发现经常需要做这种事情 , 那么也许你该考虑使用其他数据类型 , 如固定精度的小数 。
既然如此 , 为什么编程语言要在无法支持的类型上提供==比较呢?其实编程语言为每一种类型都提供了== , 程序员需要依靠自己的知识来判断哪些不能用 。
SML的实现说明(http://sml-family.org/Basis/real.html)上这样说:
判断real是否为相等的类型 , 如果是 , 那么相等本身的意义也是有问题的 。 IEEE指出 , 零的符号在比较中应当被忽略 , 而任意一个参数为NaN时 , 相等比较应当返回false 。 这些约束对于SML程序员来说非常麻烦 。 前者意味着 0 = ~0 为true , 而r/0 = r/~0为false 。 后者意味着r = r可能出现返回false的异常情况 , 或者对于ref cell rr , 可能存在 rr = rr 成立但是 !rr = !rr 不成立的情况 。 我们可以接受零的无符号比较 , 但是认为相等的反射率、结构相等 , 以及<>和not o =的等价性应当被保留 。 这些额外的复杂性让我们作出决定 , real不是具有相等性的类型 。
通过禁止real拥有=运算 , SML强迫开发者思考他们真正需要什么样的比较 。 我认为这个特性非常好!
F#提供了[]属性 , 来标记那些=不应该被使用的自定义类型 。 遗憾的是 , 他们并没有将float做上标记!
『软件工程』计算机界 TOP 3 难题:“相等”是软件工程中许多重大问题的根源
本文插图
常见错误:不相等的“相等”
PHP有两个单独的运算符:==和=== 。 ==的文档将其称为“相等” , 并记载到“如果在类型转换后$a等于$b则返回TRUE” 。 不幸的是 , 这意味着==运算符是不可靠的:
var_dump("608E-4234" == "272E-3063"); // true?> 尽管这里比较的是字符串 , 但PHP发现两者都可以被转换为数字 , 所以就进行了转换 。 由于这两个数字非常小(例如第一个数字是608 * 10^-4234) , 而我们之前说过 , 浮点数比较非常困难 。 将这两者都转换成浮点数float(0)将导致它们被四舍五入成同一个值 , 因此该比较返回真 。


推荐阅读