写点什么

编程语言中 null 引用的十亿美元错误趣谈

作者:Jerry Wang
  • 2022-10-13
    上海
  • 本文字数:1947 字

    阅读完需:约 6 分钟

编程语言中 null 引用的十亿美元错误趣谈

托尼·霍尔(Tony Hoare), 计算机科学家,因程序设计语言定义与设计方面的杰出贡献获得 1980 年的图灵奖。快速排序算法的发明者。



本文不会讨论霍尔发明的快速排序算法,而是介绍另一个来自霍尔,如今仍然被程序员在编程语言中广泛使用的一个设计:null 引用。



null 引用被霍尔称为"十亿美元错误",是霍尔 1965 年设计 ALGOL W 语言时提出的。


《Java 实战》中提到,在 Java 程序开发中使用 null 会带来理论和实际操作上的种种问题:


  • 它是错误之源

  • 会使你的代码膨胀

  • 自身毫无意义

  • 破坏了 Java 的哲学

  • 在 Java 的类型系统上开了口子


霍尔的名言:


我把它叫做我的“十亿美元错误”,就是在 1965 年发明了空引用...... 我无法抵挡放进一个空引用的诱惑,仅仅是因为实现起来非常容易。


引入了空引用的编程语言,在访问引用之前,需要显式检查引用是否有效。

Java

下图第 46 行代码定义的 print 方法,输入参数是一个类型为 Integer 的引用。在调用引用之前,需要先判断其是否是空引用,否则程序执行时就会出现运行时异常。


ABAP

使用 CHECK X IS NOT INITIAL 进行防御,如果 X 为空引用,则不会执行 CHECK 语句的下一条语句。



严谨的德国人,在霍尔教授 null 引用的基础上,又设计出 IS BOUND, IS NOT INITIAL 和 IS ASSIGNED 这几种判断逻辑。

  • IS BOUND: 它检查引用变量是否包含有效引用。 另一方面,即使引用数据对象从堆栈中删除,包含堆栈引用的数据引用变量也可能变得无效。

  • IS ASSIGNED: 检查是否将内存区域分配给 field symbol. 如果字段符号指向内存区域,则表达式为真。

  • IS INITIAL: 检查操作数操作数是否为初始值。 如果操作数包含其类型兼容的初始值,则表达式为真。

JavaScript

第 10 行的 print 方法内部,用 && 操作符的短路逻辑(short-circuit)特性来实现空引用的检测:如果传入的 oPrinter 是空引用,则不会执行 && 后面的 print 调用。



而 TypeScript 提供的可选链(Optional Chaining),则可以在语言层面优雅地避免这个问题。


下面的 TypeScript 代码,使用问号构造了一个可选链。如果 a 为空,则表达式 a?.b 直接返回 undefined 给变量 val,而不会试图去执行 a.b



上图 TypeScript 代码,编译之后生成的 JavaScript 代码如下图所示,我们可以把 TypeScript 的可选链看成 JavaScript 用三元表达式实现的语法糖。



为了减轻 Java 程序员每次使用引用之前,显式进行非空检查的工作量,Java 8 引入了一个新的工具类:Optional.


Optional 仅仅是一个不含任何业务逻辑的包裹类,其 value 字段指向了真正的业务类。



下图是一个使用 Optional 工具类的例子,第 11 行的 filter 方法,传入的是一个通过 Lambda Function 实现的过滤条件。这行语句的语义是,对 anotherName 包含的字符串,进行过滤操作,检查 another 实例的 value 字段存储的引用,是否满足过滤条件(字符串长度小于 6):



Optional.filter 方法,无论过滤条件是否满足,返回的类型均为 Optional,便于链式调用。


我第 10 行传入 Optional 对象的字符串,显然长度远远大于 6,所以 filter 方法返回一个新的 Optional 对象,其 value 字段为 null.


对于 filter 调用返回的 Optional 对象,我们可以继续调用 orElse,设置一个默认值。下图第 14 行用 orElse 实现的逻辑,语义是:如果 shortName 包裹的 value 字段为空,则返回 orElse 方法传入的默认值。



Java 8 的 Optional 工具类并不像 TypeScript 的可选链一样,后者是语言层面提供的特性,而 Optional 仅仅是开发包里的一个工具类。


比如 Optional 的静态方法 of,其实现仅仅是新建一个 Optional 对象,去包裹传入的 value 引用:



orElse 方法,内部实现也是一个简单的三元表达式。



看这样一个极端的例子:


Outer 类有一个字段 nested,类型为 Nested 类。Nested 类有一个字段 inner,类型为 Inner 类。Inner 类包含了字段 foo,类型为 String,值为 Jerry:



如果想从 Outer 类的实例出发,写一段比较健壮的代码,打印出深藏在 Inner 类里的 foo 字段,常规的写法和使用 Optional 的写法分别位于下图 test1 和 test2 方法,大家可以比较下,更喜欢哪一种?



值得一提的是,类似 Java Optional.orElse 方法,在 ABAP 里也存在基于语言层面的支持。


下图是 ABAP 740 的新语法:



上面的新语法,翻译成传统的 ABAP 代码如下:



由此可见,新的 ABAP 内表读取的语法比较简洁,能少写 3 行代码。


但是新语法有一个缺陷:如果 it_data 内表,不存在 object_ext 的值为 cl_crm_prodil_bo_names=>gc_prod_root 的记录,此时程序执行会被终止,抛出异常 CX_SY_ITAB_LINE_NOT_FOUND:



当然针对这种情况,ABAP 也有对应的解决方案。


下图测试代码第 17 行会抛出异常,而 19 行不会。从语义上容易理解:如果内表 lt_data 里不存在 name 为 Spring2 的记录,则返回开发者使用 DEFAULT VALUE 关键字指定的一个结构,作为默认值。



第 19 行执行完毕后,结构 ls3 的 name 字段为 SpringInvalid, value 为 999.


本文从霍尔教授 1965 年提出的 null 引用作为切入点,向大家分享了 Jerry 工作中同空引用打交道的一些经历,感谢阅读。

发布于: 刚刚阅读数: 3
用户头像

Jerry Wang

关注

🏆InfoQ写作平台-签约作者🏆 2017-12-03 加入

SAP成都研究院开发专家,SAP社区导师,SAP中国技术大使。2007 年从电子科技大学计算机专业硕士毕业后加入 SAP 成都研究院工作至今。工作中使用 ABAP, Java, JavaScript 和 TypeScript 进行开发。

评论

发布
暂无评论
编程语言中 null 引用的十亿美元错误趣谈_Java_Jerry Wang_InfoQ写作社区