Wait the light to fall

Rust 中的所有权

焉知非鱼

变量绑定 #

在 Rust 中, 我们将一些值绑定到一个名字上, 称之为变量绑定。使用 let 来声明一个绑定:

fn main() {
    let x = 5;
}

所有权 #

Rust 中的变量绑定有一个属性: 变量拥有它们所绑定的值的所有权。当绑定超出作用域, 变量绑定的资源就会被释放。例如:

fn foo() {
    let v = vec![1,2,3,4,5];
}

img

v 进入作用域, 会在栈上创建一个新的 vector, 并在堆上为该 vector 的 5 个元素分配空间。当 vfoo() 的末尾离开作用域时, Rust 会释放该 vector 占用的资源。

移动语义 #

在 Rust 中, 一个值一次只能有一个所有者。

let v = vec![1,2,3,4,5];       // ① 声明一个绑定
let s = v;                     // ② v moved to s
println!("v[0] is: {}", v[0]); // ③ error: use of moved value: `v`

img

除了 ② 里面发生所有权转移(move)外, 另外一种常见的的 move 语义发生在将变量作为参数传递给函数时:

fn take(v: Vec<i32>) {
    // take the ownship of `v`
}

let v = vec![1,2,3,4,5];       // ① 声明一个绑定
take(v);                       // ② 实参绑定给形参, 所有权转移到 take 函数内
println!("v[0] is: {}", v[0]); // ③ error: use of moved value: `v`

为什么移动了绑定之后我们不能再使用它们? #

#let v = vec![1,2,3,4,5];
#let mut s = v;
s.truncate(2);

假设 v 仍可以访问, 这会产生一个无效的 vector, 因为 v 不知道堆上的数据已经被截断了。现在 v 在栈上的部分与堆上的相应部分信息并不一致。v 仍然认为有 5 个元素, 并乐意我们访问那些不存在的元素 v[3], v[4]。这会导致下标越界错误, 更糟糕的是你可能访问了未经授权的数据。

这就是为什么 Rust 在所有权被移动后禁止使用原来的绑定 v 的原因。

Copy 类型 #

当所有权被转移给另外一个绑定以后, 就不能再使用原始绑定。但是并不都是如此, Copy trait 会改变这个行为。凡是实现了 Copy trait 的类型, 其所有权并不像你想象的那样遵循“所有权规则”被移动。例如 Rust 中的所有的基本类型都实现了 Copy trait。

fn main() {
    let a  = 5;
    let _y = double(a);
    println!("{}", a);
}

fn double(x: i32) -> i32 {
    x * 2
}

所有权之外 #

如果我们在的用完函数后,还想继续使用原始绑定, 我们不得不在函数中交还所有权:

fn foo(v: Vec<i32>, s: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
    // do stuff with `v1` and `v2`

    // 交还所有权和计算结果
    (v, s, 42)
}

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

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

这样函数就变得复杂了。

引用和借用 #

我们使用「借用」来改写上面的函数:

fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
    // do stuff with `v1` and `v2`

    // return the result
    42
}

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

let answer = foo(&v1, &v2);
// we can use `v1` and `v2` here!

我们来看一个具体的例子:

fn main() {
    // 借用不可变引用
    fn sum_vec(v: &Vec<i32>) -> i32 {
        return v.iter().fold(0, |a, &b| a + b);
    }

    // 借用两个 Vector 并使它们相加
    // 这种借用不允许对所借引用进行更改
    fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
        let s1 = sum_vec(v1);
        let s2 = sum_vec(v2);

        // 返回答案
        s1 + s2
    }

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

    let answer = foo(&v1, &v2);
    println!("{}", answer);
}

在函数 sum_vec 和函数 foo 中, 我们接收引用 &Vec<i32> 作为参数, 而不是 Vec 作为参数。在调用函数的时候, 我们传递引用 &v1&v, 而不是直接传递 v1v2。借用变量的绑定在它离开作用域时并不释放资源。这意味着, 在调用 foo() 之后, 我们可以再次使用原始绑定。

引用是不可变的。那么在 foo() 中, vector 不能被改变:

fn foo(v: &Vec<i32>) {
    v.push(6);
}

let v = vec![];
foo(&v);

&mut 引用 #

&mut T, 可变引用, 允许你改变你借用的资源。例如:

let mut x = 5;

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

y 是 x 的一个可变引用, 接着把 y 指向的值加 1。注意, x 也必须被标记为 mut, 因为不能获取一个不可变值的可变引用。

y 前面的星号(*) 是解引用, 因为 y 是一个 &mut 引用, 需要使用星号来访问引用的内容。

但是这样写你不觉得奇怪吗?为什么还要使用额外的花括号呢?因为我们需要额外的作用域, 如果移除花括号, 则会编译报错:

error: cannot borrow `x` as immutable because it is also borrowed as mutable
    println!("{}", x);
                   ^
note: previous borrow of `x` occurs here; the mutable borrow prevents
subsequent moves, borrows, or modification of `x` until the borrow ends
        let y = &mut x;
                     ^
note: previous borrow ends here
fn main() {

}
^

啊,好像越来越复杂了,其实 Rust 中的借用要符合几个规则:

第一, 任何借用持续的作用域必须比所有者更小。第二, 同一作用域下, 要么只有一个对资源 A 的可变引用(&mut T), 要么有 N 个不可变引用(&T), 但不能同时存在可变和不可变的引用。

作用域 #

让我们移除上面代码中的花括号:

fn main() {
    let mut x = 5;
    let y = &mut x;

    *y += 1;

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

编译报错:

error: cannot borrow `x` as immutable because it is also borrowed as mutable
    println!("{}", x);
                   ^

因为我们违反了规则:在同一作用域下,不能同时存在可变和不可变引用。在这个例子中, 我们有一个指向 x 的 &mut T, 所以我们不允许创建任何 &T

错误记录提示了我们应该如何理解这个错误:

note: previous borrow ends here
fn main() {

}
^

可变借用一直持续到 main 函数的末尾花括号处。我们需要可变借用在我们尝试调用 println! 之前结束并生成一个不可变借用。我们的作用域看起来像这样:

fn main() {
    let mut x = 5;

    let y = &mut x;    // -+ &mut borrow of `x` starts here.
                       //  |
    *y += 1;           //  |
                       //  |
    println!("{}", x); // -+ 试图从这儿借用 `x`
}                      // -+ &mut borrow of `x` ends here.

作用域冲突了, 所以要加花括号:


#![allow(unused_variables)]
fn main() {
  let mut x = 5;

  {
    let y = &mut x; // -+ &mut borrow starts here.
    *y += 1;        //  |
  }                   // -+ ... and ends here.

  println!("{}", x);  // <- Try to borrow `x` here.
}

借用避免了什么 #

避免迭代器失效 #

当你尝试改变你正迭代的集合时, Rust 的借用检查器会阻止你:

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

for i in &v {
    println!("{}", i);
}

我们对 v 进行迭代, 我们只得到了元素的引用。v 本身是不可变借用, 这意味着我们在迭代时不能改变它:

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

for i in &v {
    println!("{}", i);
    v.push(34); 
}

编译报错:

error: cannot borrow `v` as mutable because it is also borrowed as immutable
    v.push(34);
    ^
note: previous borrow of `v` occurs here; the immutable borrow prevents
subsequent moves or mutable borrows of `v` until the borrow ends
for i in &v {
          ^
note: previous borrow ends here
for i in &v {
    println!(“{}”, i);
    v.push(34);
}
^

我们不能修改 v 是因为它被循环借用。

释放后使用 #

引用必须与它所引用的值存活的一样长。Rust 会检查引用的作用域:

let y: &i32;       // ①
{                  // ②
    let x = 5;     // ③
    y = &x;        // ④
}                  // ⑤

println!("{}", y); // ⑥

这会编译报错:

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;
}

也就是说, y 只在 x 存在的作用域中有效。一旦 x 消失, y 就变成无效引用。这个错误说, x 活的不够久, 因为它在应该有效的时候(⑥)是无效的。

当引用在它所引用的变量之前声明也会导致类似的问题。这是因为同一作用域中的资源以它们被声明的相反的顺序被释放:

let y: &i32;
let x = 5;
y = &x;

println!("{}", y);

会报编译错误:

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;

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

note: ...but borrowed value is only valid for the block suffix following
statement 1 at 3:14
    let x = 5;
    y = &x;

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

在上面的例子中, y 在 x 之前被声明, 这意味着 y 比 x 的生命周期更长, 这是 Rust 规则所不允许的。

生命周期 #

想象一下下列操作:

  • ① 我获取了某种资源的句柄
  • ② 我借给你一个关于这个资源的引用
  • ③ 我决定不再需要这个资源了, 并且释放了它, 而你仍然持有对它的引用
  • ④ 你要使用这个资源

奥! 你的引用指向一个无效的资源。如果这个资源是内存, 则这叫「释放后使用」。例如:

let r;             // ⓪ 引入引用 `r`

{
    let i = 1;     // ① `i` 是值 1 的所有者
    r = &i;        // ② 把 `i` 借给 `r`
}                  // ③ `i` 超出作用域, 被删除

println!("{}", r); // ④ `r` 仍然指向 `i`

在 ④ 之后, 变量 i 超出作用域被释放, 而 ⑤ 中, 打印 r 指向的值, 但是 r 所借用的 i 已经在内存中被删除了。所以 r 现在指向的内存中, 已经不是 i 了。

Rust 不仅检查不同变量作用域, 还检查不同变量的生命周期。

当我们将引用作为函数的参数时, 情况变得更复杂了:

fn skip_prefix(line: &str, prefix: &str) -> &str {
    // ...
    # line
}

let line = "lang:en=Hello World!";
let lang = "en";

let v;
{
    let p = format!("lang:{}=", lang); // -+ `p` 进入作用域
    v = skip_prefix(line, p.as_str()); // |
}                                      // -+ `p` 超出作用域
println!("{}", v);

skip_prefix 函数接收两个 &str 引用作为参数并返回一个 &str。通过传入 line 和 p 的引用来调用 skip_prefix 函数, 但是 line 和 p 是两个具有不同生命周期的变量。现在 println! 那行代码的安全完全依赖于 skip_prefix 函数返回的引用是仍然存活的 line 还是已经被释放掉的 p。

因为存在上述歧义, Rust 会拒绝编译这段代码。为了继续我们需要向编译器提供更多关于引用的生命周期的信息。这可以通过在函数签名中显式地标明生命周期来完成:

fn skip_prefix<'a, 'b>(line: &'a str, prefix: &'b str) -> &'a str {
    // ...
    # line
}

函数名后面的 <'a, 'b> 引入了两个生命周期参数 'a'b。 接下来的函数签名中的每个引用都关联了一个生命周期参数, 这是通过在 & 后面加上生命周期的名字来完成的。

这样编译器能推断出 skip_prefix 函数的返回值与 line 参数有着相同的生命周期, 这样就使得之前例子中的 v 引用即使在 p 离开作用域之后也能安全使用。

生命周期注解是 descriptive(描述性的), 这意味着引用的生命周期是由代码决定的。

语法 #

'a 读作"生命周期 a"。技术上讲, 每一个引用都有一些与之相关的生命周期。一个显式生命周期的例子:

fn bar<'a>(...)

一个函数可以在 <> 之间有"泛型参数", 生命周期也是其中一种。

我们用 <> 声明了生命周期。bar 有一个生命周期 'a。如果我们有两个拥有不同生命周期的引用作为参数, 它应该看起来像这样:

fn bar<'a, 'b>(...)

接着在我们的参数列表中, 我们使用了我们命名的生命周期:

...(x: &'a i32)

如果我们想要一个 &mut 引用, 我们这么做:

...(x: &'a mut i32)

&mut i32&'a mut i32 是一样的, 只是后者在 &mut i32 之间夹了一个 'a 生命周期。&mut i32 读作"一个 i32 的可变引用", 而 &'a mut i32 读作"一个带有生命周期 ‘a 的 i32 的可变引用"。

在 struct 中 #

当处理结构体时, 你也需要显式的生命周期:

struct Foo<'a> {
    x: &'a i32,
}

fn main() {
    let y = &5; // 这等价于 `let _y = 5; let y = &_y;`
    let f = Foo { x: y };
    println!("{}", f.x);
}

struct 名字的后面声明了一个生命周期 <'a>:

struct Foo<'a>

接着在结构体中使用它:

#struct Foo<'a> {
    x: &'a i32,
#}

然而为什么这里需要一个生命周期呢?因为我们需要确保任何 Foo 的引用不能比它包含的 i32 引用活的更久。

impl 块 #

让我们给 Foo 结构体实现一个方法:

struct Foo<'a> {
    x: &'a i32,
}

impl<'a> Foo<'a> {
    fn x(&self) -> &'a i32 { self.x }
}

fn main() {
    let y = &5;
    let f = Foo { x: y };
    println!("x is {}", f.x());
}

我们需要在 impl 关键字后面为 Foo 声明一个生命周期。就像在函数中那样, 我们重复了 'a 两次: impl<'a> 定义了一个生命周期, 然后 Foo<'a> 使用了它。

多个生命周期 #

如果你有多个引用, 你可以多次使用同一个生命周期:

fn x_or_y<'a>(x: &'a str, y: &'a str) -> &'a str {
#    x
#}

这意味着在同一作用域内, x 和 y 的存活时间一样久, 并且返回值和 x 与 y 的存活时间一样久。如果需要 x 和 y 有不同的生命周期, 可以使用多个生命周期参数:

fn x_or_y<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
#    x    
#}

在这个例子中, x 和 y 有不同的有效作用域, 而返回值和 x 有着相同的生命周期。

理解作用域 #

理解生命周期的一个办法是想象出引用的有效作用域的示意图。例如:

fn main() {
    let y = &5;           // -+ `y` 进入作用域
    
    // Stuff ...          //   |
                          //   |
                          //   |
}                         //  -+ `y` 超出作用域

再加入我们的 Foo:

struct Foo<'a> {
    x: &'a i32,
}

fn main() {
    let y = &5;           //  -+ `y` 进入作用域
    let f = Foo { x: y }; //  -+ `f` 进入作用域
    // Stuff ...          //   |
                          //   |
                          //   |
}                         //  -+  `f` 和 `y` 先后超出作用域

我们的 f 生存在 y 的作用域之中, 所以一切正常。那么如果不是呢:

struct Foo<'a> {
    x: &'a i32,
}

fn main() {
    let x;                     //  -+ `x` 进入作用域
                               //   |
    {                          //   |
         let y = &5;           //  -+ `y` 进入作用域
         let f = Foo { x: y }; //  -+ `f` 进入作用域
             x = &f.x;         //   | | 这引发错误
    }                          //  -+ `f` 和 `y` 先后超出作用域
                               //   |
    println!("{}", x);         //  -+ `x` 超出作用域                  
}

如上面的作用域示意图所示, fy 的作用域小于 x 的作用域。但是, 在 x = &f.x 这里, 我们让 x 引用了将要离开作用域的变量, 这会导致"释放后使用"。

就像命名正则表达式一样, 命名作用域赋予作用域一个名字。有了名字我们就可以谈论它了。

‘static #

名字叫 static 的生命周期是特殊的。它代表某个东西具有横跨整个程序的生命周期。我们已经见到过:

let x: &'static str = "Hello, world.";

基本字符串是 &'static str 类型的, 因它的引用一直有效: 它们被写入了最终库文件的数据段。另外一个例子是全局变量:

static FOO: i32 = 5;
let x: &'static i32 = &FOO;

它在二进制文件的数据段中保存了一个 i32, 而 x 是它的一个引用。