Null 引用一直是个坏主意,从来没发挥过什么正面作用。
2020 年是 ALGOL 60 的 60 周年诞辰。ALGOL 60 让结构化编程真正落地,并为 Pascal、C 语言、B 语言和 Simula 的出现打下了坚实基础,可以称之为是编程语言们的“祖父”。
Null 的产生是由于 1965 年的一个偶然事件。
托尼·霍尔(Tony Hoare)是快速排序算法的创造者,也是图灵奖(计算机领域的诺贝尔奖)的获得者。他把 Null 添加到了 ALGOL 语言中,因为它看起来很实用而且容易实现。但几十年后,他后悔了。
Tony 表示,1965 年把 Null 引用加进 ALGOL W 时的想法非常简单,“就是因为这很容易实现。”
但如今再次谈到当初的决定时,他表示这是个价值十亿美元的大麻烦:
“ 我称之为我的十亿美元错误……当时,我正在设计第一个全面的类型系统,用于面向对象语言的引用。我的目标是确保所有对引用的使用都是绝对安全的,由编译器自动执行检查。但是我无法拒绝定义一个 Null 引用的诱惑,因为它实在太容易实现了。这导致了无法计数的错误、漏洞和系统崩溃。在过去的四十年里,这些问题可能已经造成了十亿美元的损失。”
这就是这个“十亿美金的错误”的起源。现在有些语言开始解决这个问题。
解决方案
NULL 变得如此普遍以至于很多人认为它是有必要的。NULL 在很多低级和高级语言中已经出现很久了,它似乎是必不可少的,像整数运算或者 I/O 一样。 不是这样的!你可以拥有一个不带 NULL 的完整的程序语言。NULL 的问题是一个非数值的值、一个哨兵、一个集中到其它一切的特例。 相反,我们需要一个实体来包含一些信息,这些信息是关于(1)它是否包含一个值和(2)已包含的值,如果存在已包含的值的话。并且这个实体应该可以“包含”任意类型。这是 Haskell 的 Maybe、Java 的 Optional、Swift 的 Optional 等的思想。 例如,在 Scala 中,Some[T]
保存一个T
类型的值。None
没有值。这两个都是Option[T]
的子类型,这两个子类型可能保存了一个值,也可能没有值。
那么在Go里面如何解决这个问题呢?
我想到了三种方式,当然都不完美,但是等到Go支持范型后,第二个方案将变得很合适。
方案一
由于Go语言支持多返回值,所以我们可以通过返回形如(Value, Exists)这样的两个两个返回值;而在使用Value之前,先判断Exists的值是否为true来决定Value值是否可用。如代码所示:
func GetItem(id int) (Type, bool) {
return Value, Exists
}
value, exists := GetItem(id)
if !exists {
fmt.Printf("Id:%d doesn't exist.", id)
return
}
// do something with value
优点:简单明了。
缺点:有时候可能会被程序员忘记判断第二个返回值。
方案二
不直接返回所需数据,而是返回一个对象。这样,程序员更不容易忘记对数据是否存在做判断。如代码所示
type Response struct {
Value *Type
Exists bool
}
func GetItem(id int) Response {
return Response {
Value,
Exists,
}
}
response := GetItem(id)
if !response.Exists {
fmt.Printf("Id:%d doesn't exist.", id)
return
}
// do something with response.Value
优点:相比方案一,此方案会提醒程序员去判断值是否存在。
缺点:程序员仍然有可能忘记判断值是否存在。
方案三
杜绝程序员直接获得目标值的机会,取而代之的是必须先判断数据是否存在,然后才能获取到数据。代码如下所示:
type Option struct {
value interface{}
exists bool
checked bool
}
func NewOption(value interface{}, exists bool) *Option {
return &Option{
value: value,
exists: exists,
checked: false,
}
}
func (this *Option) Exists() bool {
this.checked = true
return this.exists
}
func (this *Option) GetValue() interface{} {
if !this.checked {
panic("This object has not been verified.")
}
return this.value
}
func GetItem(id int) *Option {
return NewOption(value, exists)
}
option := GetItem(id)
// option.GetValue() 直接调用获取值的方法会导致panic,因为此时Exists属性尚未被检查
if !option.Exists() {
fmt.Printf("Id:%d doesn't exist.", id)
return
}
// do something with option.GetValue().(Type)
优点:对数据进行了很好的封装,杜绝了程序员犯错的可能性。
缺点:
1、在调用GetValue()方法后需要做类型的推断,有点verbose;当Go支持范型后,此问题将可以得到解决;
2、为了处理的方便,在返回Option对象的时候,我使用了引用,这将导致每次调用都会在heap上创建一个对象,从而加大了GC的负担。