目录

Rust所有权与生命周期

在 Rust 编程语言的内存管理总共有两种,一种是 C/C++ 方式的手动管理内存,优点就是程序性能好,内存使用率高,但是缺点是手动管理内存难度高,处理不好就会出现内存溢出。 而另一种则是 Java 方式的 GC (内存回收机制),每隔一段时间调用 gc 线程将不需要的内存资源回收,优点是开发者不用关注内存,专注于使用编程语言实现需要的功能,缺点是不好 控制内存回收,造成内存性能较大。而 Rust 则是另辟蹊径,引入所有权和生命周期来管理内存。

什么是所有权

所有权(Ownership)是 Rust 中的新增概念。程序中的每个值都有对应的变量指向它,而在 Rust 中变量更是拥有这个值的所有权。Rust 中规定了三条所有权规则:

  • 每个值在 Rust 中都有一个变量来管理它,这个变量就是这个值、这块内存的所有者。
  • 每个值在一个时间点上只有一个管理者。
  • 当变量所在的作用域 (scope) 结束的时候,变量以及它代表的值将会被销毁。
1
2
3
4
5
6
fn main() {
    // 变量 v 拥有 3 这个值的所有权
    let v = 3;

    // 作用域结束,变量 v 被销毁,3 这块内存将被回收
}

所有权转移

变量在作用域结束后会被销毁,对应的内存会被回收,那有什么方式可以让内存被继续使用呢?答案是所有权的转移。

所有权的转移发生在变量赋值时,其中包括赋值语句、函数调用、函数返回。

1
2
3
4
5
6
7

fn main() {

    let s = "hello world".to_string();
    // 发生所有权的转移,现在 s1 拥有 3 的所有权,使用变量 s 时将会报错。
    let s1 = s;
}

所有权的转移有两种方式,Move 和 Copy。

移动 (Move)

所有权转移的默认语义是移动 (Move),即所有权转移后,源变量将被销毁,这个符合所有权第二条规则。

复制 (Copy)

std::marker::CopyCopySendSizedSync 四个特殊 trait 中的一个,如果一个变量的类型实现了 Copy trait。它的默认转移语义就会变成复制 (Copy)。 即在赋值语句时,源变量不是直接将所有权交给新的变量,而是先对原来的内存执行一次深拷贝后交给新的变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
    let p = P::new("p");

    let p1 = p;

    println!("p name is {}", p.name);
    println!("p1 name is {}", p1.name)
}

#[derive(Clone, Copy)]
struct P {
    name: &'static str,
}

impl P {
    fn new(name: &'static str) -> Self {
        P {
            name: name,
        }
    }
}

Rust 的变量可以保存在堆和栈上,值保存在栈的变量的默认转移语义为 Copy,值保存在堆上的变量的默认转移语义为 Move。

1
2
3
4
5
6
7
fn main() {
    let v = "s"; // &str 保存在栈上
    let v1 = v;  // 赋值后, v 变量依然有效

    let s = "s".to_string();  // String 保存在堆上
    let s1 = s;               // 赋值后会 move,所以 s 变量被销毁
}

我们可以使用 Box 将变量保存到堆上:

1
2
3
4
fn main() {
    let v = Box::new(3); // 变量保存到堆上
    let v1 = v;          // 赋值时 move
}

什么是生命周期

生命周期是针对变量而言的,它指的是变量从创建到销魂的整个过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
    let v = 1;              // v 的生命周期起始

    {
        let v1 = 2;         // v1 的生命周期起始
        println!("{}", v1); // v1 的生命周期结束
    }

    println!("{}", v);     // v 的生命周期结束
}

生命周期和所有权是相关联的,一般变量的生命周期限定在其作用域中,但如果发生所有权的转移,则作用域也发生改变,生命周期的长度就随之改变。

借用

所有权除了转移 move 之外,还可以借用 (borrow),借用的语法为

  • & 符号,表示只读借用,也可以使用 borrow() 方法。
  • &mut 符号,表示可读写借用,也可以使用 borrow_mut() 方法。

借用指针 (borrow pointer) 也可以称作"引用" (reference)。借用指针与普通指针的内部数据是一样的,区别在于语义。它的作用是告诉编译器,它对指向的这块内存区域 没有所有权。

借用规则

同样的,rust 变量借用也有它的规则:

  • 借用指针不能比它指向的变量存在的时间更长
  • &mut 型借用只能指向本身具有 mut 修饰的变量,对于只读变量,不可以有 &mut 型借用。
  • &mut 型借用借用指针存在的时候,被借用的变量本身会处于”冻结“状态。
  • 如果只有&型借用指针,那么能同时存在多个;如果存在 &mut 型借用指针,那么只能存在一个;同时存在其他和 & 或者 &mut 型借用指针,则编译错误。

生命周期标记

对一个函数内部的生命周期进行分析, Rust编译器可以很好地解决。 但是, 当生命周期跨函数的时候, 就需要一种特殊的生命周期标记符号了。

生命周期标记的语法为: 'a,单引号加字母。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn main() {
    let p = P { name: "p" };

    let name = get_name(&p);
    println!("name is {}", name);
}

struct P<'a> {
    name: &'a str,
}

fn get_name<'a>(arg: &'a P) -> &'a str {
    &arg.name
}

'static 是一种特殊的生命周期标记,它表示这个程序从开始到结束的整个阶段。

当同时存在多个生命周期标记时,可以使用 where 来约束标记之间的关系:

1
2
3
4
5
6
7
8
9
fn compare<'a, 'b>(p1: &'a P, p2: &'b P) -> &'b str
where
    'a: 'b,
{
    if p1.name.len() > p2.name.len() {
        return p1.name;
    }
    p2.name
}

Rust 中有些函数可以省略标记,因为 Rust 可以自动补全。自动补全标记的规则如下:

  • 每个带生命周期参数的输入参数,每个对应不同的生命周期参数。
  • 如果只有一个输入参数带生命周期参数,那么返回值的生命周期被指定为这个参数。
  • 如果有多个输入参数带生命周期参数,但其中有&self,&mut self,那么返回值的生命周期被指定为这个参数。
  • 以上都不满足,就不能自动补全返回值的生命周期参数。

借用检查

Rust 中引入的这些概念就是为了在没有放弃对内存的直接控制能力的情况下,实现内存安全。其中我们可以将之总结成两个概念:

  • Alias 意为“别名”。如果一个变量可以通过多种 Path 来访问,那它们就可以互相看作 alias。Alias 意味着“共享”,我们可以通过多个入口访问同一块内存。
  • Mutation 意为“改变”。如果我们通过某个变量修改了一块内存,就是发生了 mutation。Mutation 意味着拥有“修改”权限,我们可以写入数据。

所以如果能保证 alias 和 mutation 不同时出现,Rust 就认为代码是安全的。即“共享不可变,可变不共享”。

小结

Rust 是一门神奇的语言,它在不放弃对内存的直接控制能力的情况下,实现了内存安全。这个一种开创时的发明。为此它引入了几个新的概念,如所有权、生命周期、变量借用,拔高了学习成本。