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


有一个笑话说 , 计算机科学界有两大难题:一是缓存失效问题 , 二是命名问题 。 但我认为还有第三个更难的问题:相等问题 。 你没看错 , 等号“=”看似简单 , 但等号的使用和误用 , 是软件工程中许多重大问题的根源 。
声明:本文已获作者Craig Stuntz翻译授权 。
『软件工程』计算机界 TOP 3 难题:“相等”是软件工程中许多重大问题的根源
本文插图
作者 | Craig Stuntz
译者 | 弯月 , 责编 | 郭芮
头图 | CSDN 下载自视觉中国
出品 | CSDN(ID:CSDNnews)
以下为译文:
『软件工程』计算机界 TOP 3 难题:“相等”是软件工程中许多重大问题的根源
本文插图
相等的原理
我来展示一下在编程语言中相等有多么容易出错 。 但首先我要解释一下相等应有的模样 , 而这其实非常难解释!当我们讨论相等“应该如何”时 , 我们必须指出是特定语境下的相等 , 因为相等的成立有许多方式 , 许多方式在不同的语境下都是成立的 。
“数学的精髓在于 , 同一个东西可以用不同的方式呈现给我们 。 ”
——Barry Mazur , 《When is one thing equal to some other thing》
定律
我说过 , 在不同的语境下 , 相等有不同的含义 , 但尽管如此 , 有些东西是永远成立的 。 这就是相等的定律 。
相等是一个二元操作 , 它是:

  • 反射的 , 即对于任意值 a 都有 a = a
  • 对称的 , 即a = b可以推出b = a , 反之亦然
  • 传递的 , 即如果a = b且b = c , 则a = c
在编程的世界中 , 我们需要增加一条定律 , 因为有时候程序员会做一些奇怪的事情:
相等必须是:
  • 一致的 , 即如果a = b且a或b的任何字段都没有发生变化 , 那么稍后再次检查时a = b应当依然成立 。
上面的定律看似简单 , 但流行的编程语言甚至连如此简单的定律都无法遵守 。 但更严重的关于相等的问题甚至都很难精确描述 。
结构相等性
在编程语言对于相等的各种实现中 , 一个重要的区别就是结构相等和引用相等 。
结构相等会检查两个引用是否为同一个值 。 F#默认采用了结构相等:
type MyString = { SomeField : string }let a = { SomeField = "Some value" }let b = { SomeField = "Some value" }if a = b then // 返回true, 进入 "then" 代码块 但在C#中则不是这样 , C#使用的是引用相等 。 引用相等要求两个被比较的对象是同一个对象 。 换句话说 , 它会比较两个变量是否指向同一个内存地址 。 指向两个不同内存地址的引用会被判为不相等 , 即使它们的值完全一样:
class MyString { private readonly string someField; public string SomeField { get; } public MyString(string someField) => this.someField = someField;}var a = new MyString("Some value");var b = new MyString("Some value");if (a == b) { // 返回 false, 不会进入代码块 其他语言会让你选择 。 例如 , Scheme提供了equal?来检查结构相等 , eq?来检查引用相等 。 Kotlin提供了==用于结构相等 , ===用于引用相等(不要与JavaScript的==和===操作符混淆 , JavaScript的那个是完全不同的东西) 。
程序中何时应当使用结构相等?如果变量的值不会改变更 , 那么几乎任何情况下都应该使用结构相等!我所知的绝大多数编程语言在诸如integers之类的类型上都使用结构比较 。 除了Java之外 , int类型进行结构比较 , Integer类型进行引用比较 , 这迷惑了一代又一代的程序员 。 Python的is也有类似的问题 。


推荐阅读