Skip to content

zongwu's blog

让我们聊一聊Rust的所有权系统

所有权(ownership)系统是 Rust 最与众不同的特性,让Rust实现了既要保障内存安全又要无GC,运行时高性能的目标。

所有的编程语言都需要考虑内存管理机制。

一些语言提供垃圾回收机制(garbage collection,GC),例如:Java、Python、Golang、Lisp、Haskell、JavaScript 等等。 另一些语言需要编程人员手工管理内存,进行分配和释放内存。例如:C、C++。

两种机制各有优势和缺点:

带GC的编程语言,自动管理内存,消除人工管理带来的内存管理安全性问题,降低编程语言学习复杂度和使用复杂度,但是带来了额外的运行时性能开销,无法保证高性能和高实时性。

手工管理内存,运行时高性能, 但是增加编程人员的使用心智负担,容易造成内存管理安全性上的bug。

Rust选择了第三种方式,通过(自己创立的)所有权系统进行内存管理。这也是为什么最近Rust编程语言从一出现就备受瞩目的原因之一。

关于所有权系统,编译器会在编译期依据所有权规则对代码进行检查。不会给运行期带来额外的开销。(缺点是编译期阶段的时间变的很长,编译后的代码体积会膨胀(就所有权而言,实际上跟范型化的生命周期标记的实现相关))。

让我们来看看所有权规则:

Rust 所有权规则(Rust ownership rules)

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

规则1:Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。

规则2:值在任一时刻有且只有一个所有者。

规则3:当所有者(变量)离开作用域,这个值将被丢弃。

内存管理

Rust所有权规则1中的值,要么分配在栈上,要么分配在堆上。栈上的数据会随着入栈出栈操作被自动清理,编程语言主要是对堆进行内存管理。

规则1与规则2

规则1 理解起来比较简单直观,在 Rust 中通过 let 关键字将一个值绑定到一个变量上。

fn main (){
    let a = 12;
    let b = "hello";
    
    let (c,d) = ("str1","str2");
    
    println!("{},{},{},{}",a ,b ,c ,d)
}

move语义

如果将一个变量赋值给另外一个变量会如何?考虑下面这种情况:

fn main (){
   let a = String::from("hello world");
   let b = a;
   
   println!("{}",a);
   println!("{}",b);

}

编译会报错:

error[E0382]: borrow of moved value: `a`
 --> src/main.rs:5:18
  |
2 |    let a = String::from("hello world");
  |        - move occurs because `a` has type `String`, which does not implement the `Copy` trait
3 |    let b = a;
  |            - value moved here
4 |    
5 |    println!("{}",a);
  |                  ^ value borrowed here after move

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.

直接错误提示是:error[E0382]: borrow of moved value: 'a'借用已经move的值a。

同时在let b = a;那行编译器提示value moved here,在此处值被move了。

我们对let 关键字的理解更加深入:对于形如 let x = y; 的语句,如果y是变量,let 会引发变量的所有权转移。

也即是:let操作默认是move语义。 从而保障了规则2的约束。

Copy trait

新的问题出现,下面这段代码没有遵循move语义,但编译成功:

fn main (){

   let a = 128;
   let b = a;// move ?
   
   println!("{}",a);//128
   println!("{}",b);//128

}

那是因为考虑到使用上的便利性,在有些场景下并不希望应用默认的move语义。Rust的基础数据类型都实现了 std::marker::Copy trait,所以在进行let操作时,实际上发生了copy

哪些类型是满足Copy特性的?类似整型这样的存储在栈上的类型、不需要分配内存或某种形式资源的类型。常见的Copy 类型如下:

  • 所有整数类型,比如 u32。
  • 布尔类型,bool,它的值是 true 和 false。
  • 所有浮点数类型,比如 f64。
  • 字符类型,char。
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。

Clone trait

保留原来的变量,并且将值内容拷贝给新的变量,对简单的数字类型很容易理解接受,那如果是复杂类型或者说是自定义类型呢? 如果不希望发生所有权转移,可以使用clone。如下:

fn main (){

   let a = String::from("hello world");
   let b = a.clone();
   
   println!("{}",a);
   println!("{}",b);

}

String实现了std::clone::Clone trait。使用clone操作后两个变量持有不同的值(这两个值是不同的内存空间)。 能不能像使用基础数据类型那样方便地应用copy语义呢?可以的,如下:

#[derive(Debug,Copy,Clone)] // here!
struct Foo {
    x: i32,
    y: i64,
}

pub fn main() {
    let f1 = Foo { x: 1, y: 2 };
    let f2: Foo = f1;
    println!("p1 = {:?}", f1);
    println!("p1 = {:?}", f2);

}

当然这里的例子有一点点特殊,Foo结构体的成员都是基础类型,所以我们标记注解#[derive(Debug,Copy,Clone)] 即可满足。 如果其成员是复杂的类型,就需要实现CopyClonetrait

注意:Clone traitCopy traitsupertrait,所以任何一个实现Copy的类型必须实现Clone

所有权与函数

调用函数的时候,会将实际参传递给形式参数,这个操作在语义上与给变量赋值相似。可能会移动或者复制,遵循与赋值语句一样的规则。例如:

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 ...
                                    // ... 所以到这里不再有效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

这里没有特殊情况,很好。

引用与借用

再看如下的代码:

fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() 返回字符串的长度

    (s, length)
}

在每一次函数调用的时候,都获取所有权,然后在函数调用完成的时候,再返回所有权,这种方式略显繁琐。而这种调用在实际场景中极其常见,Rust提供了引用(references)功能来解决这个问题。

利用引用,上面的代码可以改写成:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

传给calculate_length()的是&s1而不是s1。函数的声明是calculate_length(s: &String)参数是s: &String

&就是引用。它允许你使用值但不获取其所有权。 毕竟我们需要遵循规则2。

我们把采用引用作为函数参数称为借用(borrowing)

这里并不想展开讨论不可变引用和可变引用的相关规则,那是并发编程场景下需要关注的点。

关于引用还需要特别注意的一个点是作用域。一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。如下代码是可以编译的:

fn main(){
    let mut s = String::from("hello");

    let r1 = &mut s; // 没问题
    println!("{}", r1); //r1最后使用的地方

    let r2 = &mut s; // 没问题
    println!("{}", r2);
}

编译器负责推断引用最后一次使用的代码位置。

slice

对集合一段连续元素的引用就是[slice类型]。 例如:

fn main(){
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];
    
    println!("{} {}",hello,world);
    
}

实际上,字符串字面量就是 slice

let s = "Hello, world!";

这里的 s的类型是 &str,它是一个指向二进制程序特定位置的 slice&str是一个不可变引用。

规则3

当所有者(变量)离开作用域,这个值将被丢弃。

如何在合适的时机清理回收内存?当持有堆内存数据的变量离开作用域的时候,堆内存的数据将被drop掉。

编译器会自动插入drop相关的代码,在运行时需要drop 的时候调用。

这条规则的重点就是作用域了。

作用域

作用域是一个项(item)在程序中有效的范围。最常见的就是函数作用域,比如

fn main(){// s无效

    let s = "hello world";// s进入作用域
    println!("{}",s);
    
}// s离开作用域

还有块作用域,例如:

fn main(){// s无效

    let s = "hello world";// s进入作用域
    println!("{}",s);
    {
        let a = 1; //a 进入作用域
         println!("{}",a);
    }// a离开作用域
    
    //println!("{}",a); 这里会出错
    
}// s离开作用域

实际上,let关键字会隐式地开启一个作用域,对于以下代码:

fn main(){ 

    let s1 = "hello"; 
    let s2 ="world";
    
} 

利用 https://play.rust-lang.org/ 的 SHOW MIR 功能可以看到:

fn main() -> () {
    let mut _0: ();                      // return place in scope 0 at src/main.rs:1:10: 1:10
    let _1: &str;                        // in scope 0 at src/main.rs:3:9: 3:11
    scope 1 {
        debug s1 => _1;                  // in scope 1 at src/main.rs:3:9: 3:11
        let _2: &str;                    // in scope 1 at src/main.rs:4:9: 4:11
        scope 2 {
            debug s2 => _2;              // in scope 2 at src/main.rs:4:9: 4:11
        }
    }

...

main函数的作用域scope 0,在其中,let s1 = "hello"; 创建一个作用域scope 1,然后let s2 ="world"; 又在scope 1里面创建了scope 2

关于引用的特殊作用域问题,上一小节已经说明,这里不再重复。

闭包带来的问题

闭包(closures)可以从环境捕获变量,并在闭包体中使用。有三种使用变量的方式:获取所有权、可变引用、不可变引用。Rust 提供了3种Fn trait 以便编译器能够更清晰直接地处理不同场景下闭包对变量所有权的操作问题。

  • FnOnce 获取变量所有权,Once表明闭包不能多次获取同一个变量的所有权,所以只能调用一次。
  • FnMut 获取变量可变的借用值
  • Fn 获取变量的不可变的借用值

注意:FnOnceFnMut FnsupertraitFnMutFnsupertraitFnOnce的例子:

fn do_action<F>(func:F)where F:FnOnce()->usize{
    println!("disappeared variable length : {}", func());
}

pub fn main(){
    let a = String::from("hello world");
    let useless_warpper = move ||->usize{a.len() } ;
    do_action(useless_warpper);
    
    //println!("{}",a);  //这里再使用a会报错
}

使用了一个关键字move 将闭包需要捕获的变量的所有权move到闭包内。 FnMut的例子:

fn do_action<F>(mut func:F)where F:FnMut()->usize{
    println!("mutable result {}", func());
}

pub fn main(){
    let mut a = 12;
    let mut mutable_warpper = ||->usize{ a+=2;a } ;
    do_action(mutable_warpper);
    
    println!("now {}",a);
}

Fn的例子(将闭包形式简化):

fn do_action<F>(func:F)where F:Fn()->usize{
    println!("result {}", func());
}

pub fn main(){
    let a = 12;
    let square = || a*a ;
    do_action(square);
    
    println!("now {}",a);
}

引用的生命周期(lifetime)标记

大部分场景下,我们使用变量的时候,编译器能够自动推断变量的作用域并正常工作。但有时候不那么明显,特别是使用引用的很多场景下,需要手工标记变量的生命周期,帮助编译器检查引用的生命周期不会超过对象的生命周期。 例如:

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("r: {}", r);
}

编译失败:

error[E0597]: `x` does not live long enough
 --> src/main.rs:5:13
  |
5 |         r = &x;
  |             ^^ borrowed value does not live long enough
6 |     }
  |     - `x` dropped here while still borrowed
7 |     println!("r: {}", r);
  |                       - borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0597`.

这里提示说'x' does not live long enoughr引用了一个存活不够久的 xr的生命周期比x的生命周期长。

Rust 通过借用检查器(borrow checker)比较作用域来确保所有的引用都是有效的。

继续看下面的代码:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

编译错误:

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ^^^^    ^^^^^^^     ^^^^^^^     ^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0106`.

错误提示说,函数返回值包含借用值,但是签名未说明是借用了x还是y。同时给出了建议将函数签名改写成:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

实际上我们无法在编译期确定该函数到底返回x还是y,也无法知道传入参数(引用)的生命周期,这就导致编译器无法判定返回的引用是否有效。这就需要按照提示改写函数签名。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

现在函数签名表明对于生命周期 'a,函数会获取两个参数,他们都是与生命周期 'a 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 'a 存在的一样长的字符串 slice。这样Rust 的借用检查器就可以在编译期检查传给该函数的参数是否合法。

当具体的引用被传递给 longest 时,被 'a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 'a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a 标注了返回的引用值,所以返回的引用值就能保证在 x 和 y 中较短的那个生命周期结束之前保持有效。

如果结构体中使用引用,同样也需要手工标注生命周期。如下代码:

#[derive(Debug)]
struct Foo{
    str: &str,
    a : i32,
}

pub fn main(){
    let x = Foo{str: "hello",a: 1};
    println!("{:?}",x);
}

同样会编译错误:

error[E0106]: missing lifetime specifier
 --> src/main.rs:3:10
  |
3 |     str: &str,
  |          ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
2 | struct Foo<'a>{
3 |     str: &'a str,
  |

error: aborting due to previous error

For more information about this error, try `rustc --explain E0106`.

改写成带标记生命周期的形式:

#[derive(Debug)]
struct Foo<'a>{
    str: &'a str,
    a : i32,
}

pub fn main(){
    let x = Foo{str: "hello",a: 1};
    println!("{:?}",x);
}

保证编译器可以检查对比x结构体的生命周期与其成员str的生命周期,前者比后者小的时候,编译通过。

静态生命周期'static,其生命周期能够存活于整个程序期间。所有的字符串字面量值都是'static的。

好了就这些。

参考: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html