我用裸指针三十年,一共也就崩掉了三(sán)万多个程序。YEAH!

简介

Rust 是一个由 Mozilla 开发的,准则为 「安全、并发、实用」的编程语言。

其实就是一个带指针安全的现代版 C++ (大雾)。

Rust 的设计不允许空指针和悬空指针,如果有就会编译失败。

Rust 有一个检查指针生命期间和指针冻结的系统,可以用来预防在 C++ 中许多的类型错误,甚至是用了智能指针功能之后会发生的类型错误。

C++ 看了都想打人

先来看一段 Rust 的代码

1
2
3
4
5
let v = vec![1, 2, 3];

let v2 = v;

println!("v[0] is: {}", v[0]);

虽然语法跟 C++ 有区别,不过语义是类似的——声明了两个变量,并打印第一个变量中的一部分内容。

如果写成 C++ 这样的代码习以为常。

但是这段代码在 Rust 中会编译失败。

1
2
3
error: use of moved value: `v`
println!("v[0] is: {}", v[0]);
^

所有权

在 Rust 中有一个所有权的概念。失去了所有权,将不能进行读取或者修改操作。

变量绑定(变量声明,函数调用等)会获得所有权。

所有权跟作用域绑定(通常是大括号)。离开作用域后,会自动释放资源。

Rust 中也有移动语义,如果将一个变量直接赋值给另一个变量,假设没有实现 Copy trait ,所有权会被移动到新的变量。

所以上面的报错信息

1
2
3
error: use of moved value: `v`
println!("v[0] is: {}", v[0]);
^

说的是使用了移动过的变量 v。一旦变量被移动过了,就禁止使用,就算 v 能指向正确的内容。

这么做是有原因的。

假设允许编译通过,可能会出现这样危险的代码

1
2
3
4
5
6
7
let v = vec![1, 2, 3];

let v2 = v;

v2.truncate(2); // 截取前两个元素

println!("v[3] is: {}", v[3]); // BOOM!

这是因为修改了内容而原来的指针并不知道,所以按照原来看起来正确的方式最终导致了错误。

借用

有时候所有权转义会带来麻烦

1
2
3
4
5
6
7
8
9
10
11
fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
// 对 v1 和 v2 搞东搞西

// 归还所有权和返回函数结果
(v1, v2, 42)
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let (v1, v2, answer) = foo(v1, v2);

其实只是需要内容而不是所有权啊。

所以 Rust 还有借用的机制。

使用借用机制后是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
// 对 v1 和 v2 搞东搞西

// 返回结果
42
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let answer = foo(&v1, &v2);

// 之后还可以使用 v1 和 v2

使用了借用机制之后没有所有权转移,自然就不用归还所有权了。

上面的引用是常数引用,不能修改。想修改需要使用 mut 关键字

1
2
3
4
5
6
let mut x = 5;
{
let y = &mut x;
*y += 1;
}
println!("{}", x); // 6

借用机制类似与 C++ 的引用机制但是又有几个区别。

  1. 可以有多个常数级借用(&T)
  2. 只能有一个可修改借用 (&mut T)

而且一个资源只能有一种借用方式,要么是常数级借用,要么是可修改借用。

借用也是跟作用域绑定的。比如以下代码

1
2
3
4
5
6
7
8
fn main() {
let mut x = 5;
let y = &mut x;

*y += 1;

println!("{}", x);
}

会编译失败

1
2
3
4
5
6
7
8
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
^
note: previous borrow ends here
fn main() {

}
^

这是因为 x 已经借用给 y 当作可修改借用。然后 println! 传递参数的时候需要常数级借用,前面说过一个资源只能有一种借用方式。

如果改成这样

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut x = 5;
{
let y = &mut x; // y 借用开始

*y += 1;
} // y 借用结束

println!("{}", x);
}

就可以了。因为 y 出了借用的作用域,借用就会自动归还。后面不会出现借用冲突的问题。

走开!烦人的空悬指针

空悬指针(dangling pointer)通常是由释放后使用(use after free)产生的。

Rust 的借用机制会防止释放后使用发生。

1
2
3
4
5
6
7
let y: &i32;
{
let x = 5;
y = &x;
}

println!("{}", y);

编译报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
error: `x` does not live long enough
y = &x;
^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
{
let x = 5;
y = &x;
}

note: ...but borrowed value is only valid for the block suffix following
statement 0 at 4:18
let x = 5;
y = &x;
}

错误提示已经说得很清楚了。就是 x 苟活的时间不够长。因为 x 出了作用域后就不再可用,所以 y 也相应的不再可用。因此拒绝编译通过可以避免后面的释放后使用的问题。

最后

Rust 的空指针的解决方案是 option type 。这里不再赘述了。

天下没有免费的午餐。Rust 又要高性能,还要零负担抽象(诶怎么说得那么像某语言),那么就只能付出用户心智负担和编译时间的代价了。

从源头上避免空指针和空悬指针是可以做到的,如果你还觉得不可能是因为你用的不是现代编程语言啊,科科!

rust 官方入门