编程语言中 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 工作中同空引用打交道的一些经历,感谢阅读。
版权声明: 本文为 InfoQ 作者【Jerry Wang】的原创文章。
原文链接:【http://xie.infoq.cn/article/60cf48ece2ccc2dfbcb0b7a05】。文章转载请联系作者。
评论