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


为什么数学不要求两种不同的操作 , 而编程语言要求?因为在数学中可以轻易判断出语境 , 而且也并非所有语言都要求不同的运算符 。 例如 , F#中赋值和测试相等都采用= 。 尽管两者采用相同的符号 , 但赋值和测试相等是完全不同的操作 。
let aValue = http://news.hoteastday.com/a/someFunction; // 赋值if aValue = http://news.hoteastday.com/a/3 then // 测试相等 语法的选择部分出于历史原因:F#基于ML , 而ML基于数学;而JavaScript的语法基于Java→C→Algo→FORTRAN 。
用于编译FORTRAN代码的机器很难根据语法来区分两种情况 , 因此采用不同的运算符是合理的 。 于是C语言把这个“特性”带到了新的高度 , 所以你甚至可以写:
int aValue = http://news.hoteastday.com/a/someFunction; // 赋值if (aValue = http://news.hoteastday.com/a/3) { // 也是赋值! 给没有C语言经验的人解释一下:这段代码先用3覆盖aValue , 然后由于表达式aValue = http://news.hoteastday.com/a/3的值为3 , 因此if的条件为真 , 因此会继续执行if块内的代码 。 通常这种写法都是错误的 , 因此许多C程序员会将if块的条件反过来写 , 来避免造成该错误:
int aValue = http://news.hoteastday.com/a/someFunction; // 赋值if (3 == aValue) { // 测试相等// [...]if (3 = aValue) { // 语法错误:无法将 aValue 赋值给 3.
『软件工程』计算机界 TOP 3 难题:“相等”是软件工程中许多重大问题的根源
本文插图
相等性的使用错误
通过上面的说明 , 希望大家都已经明白相等性并不简单 , “正确”的实现取决于语境 。 尽管如此 , 编程语言经常会把最容易的地方搞错!很多时候 , 这是相等性与其他语言特性的组合造成的 , 如隐式类型转换 。
『软件工程』计算机界 TOP 3 难题:“相等”是软件工程中许多重大问题的根源
本文插图
常见错误:相等性不是反射的
回忆一下相等性的反射率 , 即任何值都等于它自身 , a = a 。
在.NET中 , 如果在值类型上调用Object.ReferenceEquals , 其参数会在执行方法之前分别进行打包 , 因此即使传递同一个实例 , 也会返回假:
(来自文档的例子)
int int1 = 3;Console.WriteLine(Object.ReferenceEquals(int1, int1)); // 输出 False 这意味着在任何.NET语言中 a = a 都不一定为真 , 因此不满足反射率 。
在SQL中 , 不等于自身 , 因此表达式 = (或者更可能的情况是 , SOME_EXPRESSION = SOME_OTHER_EXPRESSION时两者都可能为)会返回false 。 这会导致下面乱糟糟的语句:
WHERE (SOME_EXPRESSION = SOME_OTHER_EXPRESSION) OR (SOME_EXPRESSION IS AND SOME_OTHER_EXPRESSION IS ) 而更可能发生的情况是 , 开发者会忘记的特殊规则从而导致bug 。 一些数据库服务器的SQL语言支持IS NOT DISTINCT FROM , 它的功能才是=应该有的功能 。 (或者我应该说 , 它没有不做=应该做的事情?)否则 , 就必须使用上面例子中的SQL语句 。 最好的解决办法就是尽可能使用不允许的列 。
IEEE-754浮点数也有同样的问题 , 即NaN != NaN 。 一种解释是 , NaN表示某个不确定的“非数字”结果 , 而不同计算得出的NaN并不一定是同一个不确定的非数字 , 所以这个比较本身就是不正确的 。 例如 , square_root(-2)和infinity/infinity两者的结果都是NaN , 但显然它们不一样!有时候SQL的问题也可以类似地解释 。 这样造成的问题之一就是术语的含义过多:NaN和表示的是“未知” , 还是“不精确的值” , 或者是“缺少值”?


推荐阅读