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


另一种应该采用引用相等的情况是 , 事先知道引用相等的结果与结构相等相同 。 测试结构相等显然需要额外开销 , 如果真的需要测试结构相等 , 那么这个额外开销是正常的 。 但是 , 假设你创建了大量的对象 , 而且事先知道每个对象都是结构不相等的 , 那么花费额外开销来测试结构相等是没有必要的 , 因为仅仅测试引用相等就可以得出同样的结果 。
『软件工程』计算机界 TOP 3 难题:“相等”是软件工程中许多重大问题的根源
本文插图
相等性的表示
在实数中 , 0.999……(无限循环小数)等于1 。 注意这里说的“实数”与编程语言中的Real类型不一样 。 在数学中 , 实数是无限的 , 而编程语言中的实数是有限的 。 因此 , 编程语言中没有0.999……这样的写法 , 但没关系 , 你可以使用1 , 反正两者的值是一样的 。
这本质上是数学家在表示实数系统时采用的一种选择 。 如果在系统中加入另外一种对象 , 比如无限小的数 , 那么0.999……和1就不相等了 。
“但是这并不等于说规范可以任意确定 , 因为不接受一种规范 , 必然会导致不得不发明奇怪的新对象 , 或者不得不放弃某些熟知的数学规则 。 ”
——Timothy Gowers , 《Mathmetics: A Very Short Introduction》
类似地 , 在实数系统中 , 1/2和2/4表示同样的值 。
不要把这些“相等”与JavaScript或PHP中的“不严格”相等运算符==混淆 。 这些相等跟那些运算符不一样 , 这些相等依然遵循相等的定律 。 重要的是要认识到 , 对象的相等可以用不同的方式来表达 。
在IEEE-754浮点数系统中 , -0 = 0 。
内涵和外延
一个函数何时等于另一个函数?绝大多数编程语言会进行引用相等的比较 , 我觉得这没有问题 。 因为 , 对函数进行结构比较有什么意义呢?也许我们可以使用反射来检查函数的实现 , 看看它们实现是否一样?但怎样才叫“一样”?变量名是否必须完全一样?快速排序和归并排序是不是“一样”的函数?
因此我们说 , 只要函数对于同样的输入返回同样的输出(不管其内部实现如何) , 函数就是外延相等的 , 而如果内部定义也一样 , 则是内涵相等的 。 当然 , 这也取决于语境 。 可能在某个语境中 , 我需要常数时间的函数 , 在另一个语境中 , 速度无关紧要 。 重要的是 , 必须有语境才能定义相等 , 才能用它来比较两个函数 。
我不知道是否有哪种语言在比较函数时尝试过采用引用相等之外的方法 。 但很容易想出 , 这会很有用!(例如 , 优化器尝试移除重复的代码等 。 )你只能自己实现 , 但我不得不说 , 没有相等比较 , 总要比错误的相等比较强 。
相等和赋值
当程序员的第一天就学过 , “等于”这个名字有两种不同的概念 。 一种是赋值 , 另一种是测试相等性 。 在JavaScript中需要这样写:
const aValue = http://news.hoteastday.com/a/someFunction; // 赋值if (aValue =http://news.hoteastday.com/a/== 3) { // 测试相等 这两者本质上是不同的 。 比较返回布尔值 , 而在面向表达式的语言(如Ruby)中 , 赋值返回被赋的值 。
所以Ruby代码可以这样写:
a = b = c = 3 实际上会把3赋给变量a , b和c 。 不要在引用类型上尝试 , 很可能得不到你想要的结果!
在非面向表达式的语言(如C#)中 , 赋值没有返回值 。
在数学中 , 赋值和测试相等性都使用相等运算符:
if aValue = http://news.hoteastday.com/a/3 ... where aValue = http://news.hoteastday.com/a/someFunction (而且在数学中 , 有时候=还用于其他关系 , 如合同(congruence) 。 与数学中的其他东西一样 , 这里也要区分语境;在阅读论文或书籍时必须注意语境 。 )


推荐阅读