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

这是一段用于检查两棵二叉树是否一致的算法,为了能够顺利执行后面三行代码,必须要先检查pq 是否为“无”。 Rust 给了另一个稍微不同的思考切入点。

Rust 的答卷

在 Rust 中,有一种枚举类叫做Option<T>。它的定义如下

enum Option<T> {
  Some(T),
  None,
}

熟悉泛型的同学不用我多做介绍 T 是什么了。不熟悉的同学, T 是 Template 的缩写,可以被替换成 任何类型。Option<T> 有两个成员,Some(T)NoneSome(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

上面代码中 Ti32。我们说 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

你也许会问,为什么不能直接用 TNone 进行比较,就像 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 的安全性。