Null引用一直是个坏主意,从来没发挥过什么正面作用。
2020年是ALGOL 60的60周年诞辰。ALGOL 60让结构化编程真正落地,并为Pascal、C语言、B语言和Simula的出现打下了坚实基础,可以称之为是编程语言们的“祖父”。
Null的产生是由于1965年的一个偶然事件。
托尼·霍尔(Tony Hoare)是快速排序算法的创造者,也是图灵奖(计算机领域的诺贝尔奖)的获得者。他把 Null 添加到了ALGOL语言中,因为它看起来很实用而且容易实现。但几十年后,他后悔了。
Tony表示,1965年把Null引用加进ALGOL W时的想法非常简单,“就是因为这很容易实现。”
但如今再次谈到当初的决定时,他表示这是个价值十亿美元的大麻烦:
“ 我称之为我的十亿美元错误……当时,我正在设计第一个全面的类型系统,用于面向对象语言的引用。我的目标是确保所有对引用的使用都是绝对安全的,由编译器自动执行检查。但是我无法拒绝定义一个Null引用的诱惑,因为它实在太容易实现了。这导致了无法计数的错误、漏洞和系统崩溃。在过去的四十年里,这些问题可能已经造成了十亿美元的损失。”
在20世纪50年代,大部分代码是机器或汇编代码,而且每一台电脑都有自己的独特之处。第一代编程语言被称为“Autocode”,它们的存在是为了将一些问题(比如方程)编码并翻译成机器码,但不具备现今编程语言的大部分功能。更糟糕的是,它们的特性也不尽相同,导致系统的切换变得非常麻烦。当时国际信息处理联合会(IFIP)有一大堆委员会成员专门研究与计算相关的标准和问题。
有一个小组开始设计当时被称为“算法语言”的东西:一种用于编写算法的语言。1958 年,这种语言问世了,也就是“ALGOL 58”。然而,当工程师们开始为新系统开发编译器时,他们发现“有很多东西没有被考虑在内”。
于是就有了修订和变更。一份名为“The ALGOL Bulletin”的期刊(http://archive.computerhistory.org/resources/text/algol/algol_bulletin/ )翔实记录了参与者的辛劳历程,他们尝试着解决这门语言存在的问题和不足。这个过程与今天的开源邮件组差不多,只是采用了纸质的形式。最后,他们发布了 ALGOL 60 报告,为当时的编程语言工作奠定了基础。
语言考古:纸带上的 ALGOL 60
在 ALGOL 演化的同时,Fortran 也在并行演化,Fortran 用户很喜欢 ALGOL 里的一些想法,并把它们带了过来。几十年过去了,Fortran 仍然以科学计算为中心,而 ALGOL 则成了一种用于教授计算机科学思想的编程语言。
ALGOL 60 的故事并没有随着它的命运一起终结,而是随着受它启发的其他语言延续了下来。ALGOL W,基于 Niklaus Wirth 和快速排序发明者 Tony Hoare 提出的 ALGOL X 提议,启发了 Wirth 的 Pascal 和 Modula-2,而 Pascal 的的影响一直延续到今天。
ALGOL 60 还对组合编程语言(Combined Programming Language,CPL)产生了重大影响。CPL 诞生于 20 世纪 60 年代,但历经了 10 年才得以全部实现。CPL 反过来又催生了 Basic CPL (BCPL),B 语言的祖先,而 B 语言又进一步发展成为 C 语言。
Tony 从 1960 年开始在 Elliot’s 公司(Elliot Brothers 伦敦有限公司)担任程序员,他当时的任务是设计一种新的编程语言。 ALGOL 作为新语言的基础,舍去了“if”及“then”等比较复杂的部分 , 大多数汇编语言都很简单,因此在发生错误时,可以通过跟踪找出故障根源并快速完成诊断。
在这个过程中,Tony 发明了 Null 指针。Null 被用于(或者说是被滥用于)掩盖意外情况,代码中的错误可能要在很远的地方才能被发现,从而产生令人担忧的连锁反应。
但在使用指针调用函数时,需要声明指针的类型,并且必须检查每一次引用,否则就会引发灾难。 刚开始 Tony 还没意识到这个问题 。他 的朋友 Edsger Dijkstra 认为 Null 引用 不 是个好主意。他提醒道:“只要程序里有一个 Null 引用,那么它早晚会在你的对象结构里面惹出麻烦。”最后问题又回归了起点:在运行代码的时候,你更想要速度(不检查),还是更想要安全(有检查)。
Tony 是希望用它来表示每一种类型的成员,而用户必须在每一次引用变量时都进行 Null 检查。 事实证明,这是个错误,是个价值十亿美元的大麻烦。C#、Spec#乃至 Java 这样的现代语言都引入了非 Null 引用参数的概念并进行编译时检查,以确保代码当中不可能存在 Null 值。
重载与继承机制的存在,又导致用户很难在初次创建 Null 引用时及时执行检查。要想解决问题,我们必须以 Null 引用造成了高昂的代价作为基本事实与考量前提。
“编程语言设计者们应当为由此编写出的程序中的错误负责。”
Tony 认为编程语言的 设计者应该拥有坚实的学科基础、丰富的创造力以及良好的细节创造与掌控能力,并利用这一切保证人们能够使用新语言编写出正确的程序。语言中不应存在明显的错误,也不该存在语法陷阱。
“1969 年时的我太过乐观”,没有 想到可以使用程序中的证明与形式验证机制建立逻辑与数学模型。这也是一种良好的编程语言设计研究方法。通过查看编程语言以及编写出的程序是否拥有严格的可证明性,足以客观衡量并验证某种编程语言的使用难度。如果你创造的编程语言要求用户具有全面的编程知识才能正常使用,那么不需要用户反馈,你也知道自己的语言设计存在问题。
事实上,客户也不会给出什么反馈——编程语言设计者们往往过度自信,总觉得错误是用户的事,跟自己没有关系。 Tony 表示:“ 我曾经不认同这一点,但现在我开始意识到——编程语言的设计是一项严肃的科学工程活动,我们应该对用户犯下的错误负责。 ”
虽然后来 形势有所变化——Java 编程语言及其后继者们开始将避免错误作为语言功能设计中的重要标准之一 。但这只是一项标准,只是纸面上的要求 。最重要的标准,应该是向下兼容以往所有开发成果,也就是已经编写完成的数百万甚至数十亿行代码 。 出于商业以及历史等原因,每一种商业语言最终都会衰落;但随着思路的变化,程序员们对于证明正确性的方式产生了浓厚兴趣。生产技术、语言、检查器、分析工具以及测试用例生成器等工具正帮助他们更轻松地找到程序的正确调整方法。
美国著名政治活动家 Ralph Nader 曾经对安全措施嗤之以鼻,在他看来“只要速度够快,什么车都不安全”。 请注意,他当时说的话跟市场没有任何关系—— 那时候,客户还没有将可靠性或者安全性作为对交通工具的基本要求。但在立法与监管层面的帮助与引导之下,顾客逐渐意识到可靠性与安全性的重要意义,因此每辆在售汽车都必须符合最基本的安全要求。市场的变化,同样有可能改变程序的可靠性与编程语言的表达方式。
“病毒”的出现,要求技术人员更多地关注程序正确性,甚至使之成为一种商业需求。 病毒(也可称为恶意软件,或者蠕虫)通过入侵程序中一般不会被触及的部分来执行种种可怕的操作。正是因为将鲜少触及的部分作为目标,测试往往解决不了病毒的肆虐,或者说我们需要进行与病毒相同级别的渗透测试。
这就迫使我们努力保证程序的正确性——除了用户正常的使用方式之外,我们还得考虑到种种极端情况。要解决这个问题,测试没什么用,我们得借助分析的力量。对源代码进行类型检查分析的技术本身并不复杂,但人们正在利用种种更为复杂的推理技术筛查代码内容,检查其中是否包含诸如 null 引用这类可能引发意外灾难的元素。
“所以我愿意站出来为自己的错误负责,因为我看到其他编程语言设计者确实更有责任感。”
我们以 C 语言的设计者们为例。缓冲溢出是 C 语言中 gets 函数的直接结果,该函数不会检查字符串输入的范围。如此一来,很多早期病毒就会通过覆盖代码的返回值以侵入程序内部。
这些简单的病毒让编写恶意软件变得极为轻松。如果没有缓冲区溢出这个简单的起点,很多人可能压根想不到要对程序进行入侵。无论如何,现在已经有一大批技术过硬、才能横溢的专家在想尽办法利用程序中的漏洞,并据此编写出肉鸡程序以及恶意软件。
如果不是 C 语言中 gets 函数惹的祸,这个世界上可能根本就不会存在恶意软件。现在,单是 CodeRed 这一种病毒可能已经给世界造成了 40 亿美元的损失——它破坏了所有网络、导致企业及诸多银行的业务系统陷入瘫痪,并间接让更多组织面临灾难。这股歪风已经一发而不可收拾。这比千年虫问题更夸张,因为后者的危害估计还不到 40 亿美元 。
参考链接:
https://www.lucidchart.com/techblog/2015/08/31/the-worst-mistake-of-computer-science
https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/
领取专属 10元无门槛券
私享最新 技术干货