Rust 语言没有 NULL
, None
, null
, nil
这些表示“无”的值,那么 Rust 怎么表示和处理“无”呢?
在比较对象的时候,需要先校验两个对象的是否存在。例如下面的 Python 语句
def inorder(p, q):
if p is None and q is None: return true
if p is None or q is None: return False
if not inorder(p.left, q.left): return False
if p.val != q.val: return False
if not inorder(p.right, q.right): return False
这是一段用于检查两棵二叉树是否一致的算法,为了能够顺利执行后面三行代码,必须要先检查p
和 q
是否为“无”。
Rust 给了另一个稍微不同的思考切入点。
Rust 的答卷
在 Rust 中,有一种枚举类叫做Option<T>
。它的定义如下
enum Option<T> {
Some(T),
None,
}
熟悉泛型的同学不用我多做介绍 T 是什么了。不熟悉的同学, T 是 Template 的缩写,可以被替换成
任何类型。Option<T>
有两个成员,Some(T)
和 None
。Some(T)
表示“有值”,None
表示“无值”。
通过后面的阐述,你会更加清楚 T 的作用。
Some 表示“有”。
let some_number = Some(5);
let some_char = Some('e');
None 表示“无”。无这种概念是相对的,例如说
这世上没人可以永生
也是相对于一个人可以永生的世界而言的。所以在定义的时候, 我们要指定变量的类型,以表示“无”是相对于什么的。
let some_number: Option<i23> = None;
let some_char: Option<char> = None;
当我们要比较两个对象的时候,可以用 Some<T>
这种形式的值来进行比较,例如
let x = Some(4);
let y = Some(6);
let z: Option<i32> = None;
println!("{:?}", x == y); // false
println!("{:?}", x < z); // false
上面代码中 T
是 i32
。我们说 T 可以泛指任意类型,Rust 编译时可以推导出 Some(4)
的类型。
这段的代码展示了 Some<T>
之间不但可以相互比较,而且可以和 None
进行比较。
但是,如果我们要比较的是 Some<T>
和 T
之间的关系,就会出现编译错误。
let x = Some(4);
let y = 4;
println!("{:?}", x == y); // error[E0277]: `i32` cannot be compared to `Option<i32>`
这是因为 Some<T>
和 T
是两种不同的类型,不能进行比较。如果要比较,需要先把 T
转换成 Some<T>
。
let x = Some(4);
let y = 4;
println!("{:?}", x == Some(y)); // true
你也许会问,为什么不能直接用 T
和 None
进行比较,就像 Python 中的我们可以用任何值跟 None
比较一样。
这是因为 Rust 侧重于安全性, 不允许这种隐式的转换。
我们想象一下 T
可以和 None
比较的世界
let x = 4;
// ...some other code
if x == None {
// do something
}
上述例子中,x 是一个整数,但是却可以和 None 进行比较。这是不安全的,因为 None 是一个特殊的值,表示“无”。
这个"无"是相对于什么的呢?如果 x 是一个整数,那么 None 就是相对于整数的,但是如果 x 是一个字符串呢?我们无法保证 x
的类型,所以允许任意类型的值和 None 比较是可怕的。基于这样的思考,Rust 不允许这种隐式的转换。
我们接下来看一个例子,帮助大家理解。
现在我们要制作一款游戏, 这款游戏允许我们通过键盘的 w, s, a, d 四个键控制游戏中角色的前进、后退、以及左右移动。除此之外我们不希望接受其他输入。
其中某段代码可能是这样的:
//...
enum Movement {
Up,
Down,
Left,
Right,
}
fn get_movement() -> Option<Movement> {
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
match input.trim() {
"w" => Some(Movement::Up),
"s" => Some(Movement::Down),
"a" => Some(Movement::Left),
"d" => Some(Movement::Right),
_ => None,
}
}
// ...
我们有一个 Movement
的枚举类,里面的元素分别对应四个键位,以及一个 get_movement
函数。当我们调用 get_movement
时,返回从键盘读取到的内容。内容总是不固定的。除了四个键以外,还有其他输入的可能,例如 j , k等 ,所以我们的返回类型是 Option<Movement>
。这意味着返回的要么是 Some(Movement)
要么是 None
。
这样有一个好处,调用这个函数的地方得到的总是“有”或者“无”,而且在“有”的情况下,总是拿到Some(Movement)
而不是 Some(char)
, Some(i32)
或者别的什么。例如在代码某处,我想记录用户的错误操作,我可以这样写
let mv = get_movement();
if let None = mv {
// log invalid movement
}
总结
Rust 这么做的一直接影响是操作被分成两类,一种是确定状态的,只有“有”这种情况的。
在这种场景下,“无"是不存在的,我们总是可以假设有值。
另一种是可能“有”也可能“无”的。比较的对象是不确定的, 我们不知道我们在比较两个同类型的"有”,在比较"有"和"无",还是在比较两个不同类型的"无"等等
这样的隔离保证了 Rust 的安全性。