谈谈rust的Option枚举和空指针
由于之前工作一直都在用c/c++,最近在学rust的时候发现rust里面没有空值(null),而是采用了一个Option
枚举,感觉非常有意思。
空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
这句话有点绕。实际上用过c++的同学们应该知道c++内的NULL的值是0,例如int *ptr = NULL
,这里ptr的值就是0,这里的0不是实际意义上的0,而是用来表示一个空值,ptr是一个空指针。但是我们知道计算机里没有空这种东西,所以c++取了0来表示逻辑意义上的空。之所以取0,是因为在大多数操作系统里,程序是不允许访问地址为0的内存的,因为该内存是操作西用保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。当然现在c++有nullptr了,关于nullptr和NULL,0的关系我这里就不讲了…不是我的重点。
之前提到,rust里面是没有空值的,那么空值有什么问题呢?它最大的问题就在于如果像使用非空值一样使用一个空值,会导致错误。然而空和非空的属性无处不在,所以非常容易出现这种错误。但是空值的概念仍然是有意的:
空值是一个因为某种原因目前无效或缺失的值。
因此虽然rust没有空值,但是仍然存在一个可以表达存在或不存在的概念的枚举,Option<T>
。
enum Option<T> {
Some(T),
None,
}
我们注意到,这里使用了范型。也就是说Option
枚举的Some
成员可以是任意的数据类型。需要注意的是如果使用None
而不是Some
时,需要告诉rustOption<T>
的类型。因为编译器只通过None
是无法推断出Some
的类型的。即
let x: Option<i32> = None;
这里的None
就是和空值表达类似的东西。那为什么要使用None
而不是空值呢?这里有一个很重要的一点,Option<T>
和T
不是同一个类型。不要小看着一个简单的区别,这意味着Option<T>
和T
是不能直接进行运算的,即Option<i32>
和i32
是不能直接相加的。实际上,更进一步的,在对Option<T>
进行T
的运算时,必须先将Option<T>
转化成T
类型。如此一来就可以帮助我们避免以为值非空而实际为空的情况。例如下面这段代码:
fn main() {
let a: i32 = 1;
let b: Option<i32> = Some(5);
let c = a + b;
println!("{}", c);
}
编译器会报如下错误
error[E0277]: cannot add `std::option::Option<i32>` to `i32`
--> src/main.rs:4:15
|
4 | let c = a + b;
| ^ no implementation for `i32 + std::option::Option<i32>`
|
= help: the trait `std::ops::Add<std::option::Option<i32>>` is not implementedfor `i32`
error: aborting due to previous error
我们必须先将b从Option<i32>
类型转化为i32
才能进行i32
类型的运算。如下
fn main() {
let a: i32 = 1;
let b: Option<i32> = Some(5);
let c = a + b.unwrap();
println!("{}", c);
}
另外,每当我们引入一个可能为空的值时,我们必须先把它放到Option<T>
里。当我们使用这个值时,我们必须先处理值为空的情况。也就是说,只要一个值不是Option<T>
类型的,我们就可以认定它的值不为空。
这个设计相当有意思,我又查了一些资料发现scala里就存在这个设计,rust应该就是借鉴的scala的做法,无怪乎有人说rust参考了c/c++和haskell/scala两类语言。如果有机会应该多见识一下其他的语言,开阔下思路(虽然工作估计还是c/c++,(-_-||)