Rust 入门-资源转移

108 阅读6分钟

移动与拷贝

在 Rust 中,如果一个类型实现了 Copy trait,那么该类型的值如果发生资源转移,默认会进行拷贝。例如常用的数字类型(u8,u16等), 只读引用等都实现了该 trait,例如:

fn main() {
   let x = 1;
   let y = x; // copy here
   println!("{} {}", x, y); // x 仍然可以访问
   let a = "hello";
   let b = a; // copy here, 注意仅仅是 copy 引用,而不是字符串
   println!("{} {}", a, b); // a 任然可以访问
   let c = String::from("hello"); // - move occurs because `c` has type `String`, which does not implement the `Copy` trait
   let d = c; // - value moved here
   println!("{} {}", c, d); //error! value borrowed here after move,无法访问 c,因为所有权已经转移给 d。
}

由于 String 并未实现 copy trait,因此在赋值时默认行为为 move 而不是 copy,println 已经无法访问失去所有权的 c。

移动发生时机

移动并不是只有赋值时才会发生,还包括传递参数模式匹配以及闭包:

struct People {
    sex: String,
    name: String,
}
fn print_people(p: People) {
    println!("{:?}", p)
}
fn main() {
    // 传递参数时移动
    let p = People {
        name: String::from("hello"),
        sex: String::from("b"),
    };
    print_people(p); // 资源移动

    // 模式匹配时移动
    let p = People {
        name: String::from("hello"),
        sex: String::from("b"),
    };
    match p {
        People { sex, name } => println!("{}", name), // 资源移动
        _ => (),
    }
    
    let x = String::from("hello");
    {
        let y = ||x; // 被 lambda 表达式捕获,资源移动
    }
    println!("{}", x); // 无法在此处使用
}

对于结构体来说,甚至调用方法也可能导致该结构体本身被 move:

struct MyMove;

impl MyMove {
    fn moved(self) -> Self{
        return self; // 通过返回值移动了资源 
    }
}

fn main() {
    let me = MyMove;
    me.moved(); 
    let me2 = me; // 无法访问被移动的资源
}

另一个常见的例子为使用 into_iter 会移动数组本身。

fn main() {
    let v = vec![String::from("1"), String::from("2")];
    for i in v.into_iter(){ // `v` moved due to this method call
        println!("{}", i);
    }
    let v1 = v; // value used here after move
}

部分移动

对于复杂类型,例如结构体来说,资源移动可能只发生在某一个部分,而不是整体,例如:

fn main() {
    let p = People{
        name: "linex".to_string(),
        sex: "g".to_string()
    };
    let name = p.name; // 仅移动 name
    println!("{} {}", name, p.sex); // p.sex 仍然可以访问
    let p1 = p; // value used here after partial move,但是 p 已经不可访问了
}

需要注意的是,部分移动对 Vec 类型并不生效。

引用

在不需要转移资源时可以使用引用,引用并不持有资源:

fn main() {
    let v = vec![String::from("a"), String::from("b")];
    let c = &v;
    let d = &v;
    println!("{:?} {:?} {:?}", v, c, d); // a, b, c都可以继续访问
}

引用默认为只读的,不可以修改资源,除非显示声明为可变引用:

fn main() {
    let mut v = vec![String::from("a"), String::from("b")];
    let c = &mut v;
    c.push("c".to_string()); // 可变引用,可以修改资源
    println!("{:?}", v); // ["a", "b", "c"]
}

需要注意的是,在 Rust 中:

  1. 可以有多个只读引用
  2. 只可以有一个可变引用
  3. 可变引用与只读引用不能同时存在
  4. 可变引用未实现 Copy trait(只能存在一个可变引用,所以不能 copy)

关于第 4 点例子如下:

fn main() {
    let mut v = vec![String::from("a"), String::from("b")];
    let c = &mut v;
    c.push("c".to_string());
    let d = c;
    println!("{:?}", c); //  value borrowed here after move
}

由于引用并不真正的持有资源。因为无论是可变或者只读引用,都无法对资源进行部分移动

#[derive(Debug, Clone)]
struct People {
    sex: String,
    name: String,
}
fn main() {
    let mut p = People{
        name: "linex".to_string(),
        sex: "g".to_string()
    };
    let p1 = &mut p;
    let name = p1.name; // cannot move out of `p1.name` which is behind a mutable reference
    let name_ref = &p1.name; // 但是仍然可以访问 p1.name 的引用
}

由于引用无法进行部分移动,因此对引用使用模式匹配时,解构后得到的值也是引用:

fn main() {
    let x = (1, 2);
    let y = &x;
    match y {
        (v, u) => v // type v = &i32, u = &i32
    }
}

注意,由于在 rust 临时资源会马上被销毁,因此注意不要使用临时资源的引用,例如:

fn say_hello(name: &str) -> String{
    format!("hello {}", name)
}

fn main() {
    let hello_lily = say_hello("lily").as_str();
    println!("{}", hello_lily);
}

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:6:22
  |
6 |     let hello_lily = say_hello("lily").as_str();
  |                      ^^^^^^^^^^^^^^^^^         - temporary value is freed at the end of this statement
  |                      |
  |                      creates a temporary which is freed while still in use
7 |     println!("{}", hello_lily);
  |                    ---------- borrow later used here
  |
  = note: consider using a `let` binding to create a longer lived value

say_hello 返回的为临时资源,会直接销毁,因此无法转为引用。需要先绑定到一个变量:

fn say_hello(name: &str) -> String{
    format!("hello {}", name)
}

fn main() {
    let hello_lily = say_hello("lily");
    let hello_lily_str = hello_lily.as_str();
    println!("{}", hello_lily_str); // hello lily
}

生命周期标注

当使用引用时可以避免资源移动,但是也带来了新的问题。例如,当某个方法返回了一个应用,该应用可能有两种来源:

  1. 来自于方法内部创建的资源,实际上这在 rust 中是不可能的,因为 Rust 会检测悬垂引用并且抛出编译异常。
  2. 来源于静态生命周期的应用。
  3. 来源于方法传入的引用,有可能。

第 3 种情况会相对复杂,例如编写一个返回长度更大的字符串的方法:

fn largest(s1: &String, s2: &String) -> &String{
    if s1.len() > s2.len(){
        s1
    }else{
        s2
    }
}
fn main() {
   let s1 = String::from("1234");
   let s2 = String::from("567");
   let s3 = largest(&s1, &s2);
}

看起来完美无缺的代码实际上无法通过编译。Rust 会提示需要一个显式的生命周期:

10 | fn largest(s1: &String, s2: &String) -> &String{
   |                -------      -------     ^ expected named lifetime parameter

这是为什么呢,考虑以下情况:

fn main() {
   let s1 = String::from("1234");
   let s3;
   {
       let s2 = String::from("56789");
       s3 = largest(&s1, &s2); //s3 将会指向 s2 
   }// s2 将会在这里被销毁,因此 s3 此时为悬垂引用
   println("{}", s3);
}

但是由于返回哪个字符串是运行时决定的,对于此种情况 rust 此时无法在编译期完成生命周期的检测,因此需要手动的标注生命周期:

fn largest<'a>(s1: &'a String, s2: &'a String) -> &'a String{
    if s1.len() > s2.len(){
        s1
    }else{
        s2
    }
}
fn main() {
   let s1 = String::from("1234");
   let s2;
   let s3;
   {
       s2 =  String::from("567");
       s3 =  largest(&s1, &s2);
   }
}

此时使用了 rust 中的生命周期标注语法 <'a>,其中 'a 表示一个生命周期。而 s1: &'a String, s2: &'a String 则表示 s1, s2 都具有共同的生命周期。因此这里的 'a 表示 s1, s2 生命周期重叠的部分,或者说较短的一部分 -> &'a String 则表示返回值的生命周期不能比 'a 更长。这是很好理解的,因为如果返回值的生命周期长于参数的生命周期,则可能出现悬垂引用。此时运行可以得到结果:

22 |        s3 =  largest(&s1, &s2);
   |                           ^^^ borrowed value does not live long enough
23 |    }

可以看出,在进行标注之后,rust 在能在编译器就检测到入参 s2 的生命周期相比返回值 s3 的生命周期更短,从而抛出异常。

可能有同学看到这里会问,为什么不默认认为返回值的生命周期必须短于入参呢?考虑以下情况:

fn which_one_largest(s1: &String, s2: &String) -> &'static str{
    if(s1.len() > s2.len()){
        "first"
    }else{
        "second"
    }
}

此时,返回的 &str 是个一个字符串字面量,生命周期全局有效(标记为 'static),并不依赖于 s1 或者 s2 任意一个生命周期。因此,并不是所有情况返回值都依赖于入参,Rust 也无法帮我们做这种假设。

除方法以外,持有引用的结构体也需要进行生命周期标注,考虑以下情况:

struct TestValue<'a>{
    r: &'a i32
}
fn main() {
    let t ;
    {
        let x = 1;
        t = TestValue{
            r: &x // ^^ borrowed value does not live long enough
        };
    }
    println!("{}", t);
}

从标注来看,&x 应该比 t 活的更久,但是明显 &x 的生命周期短于 t,因此 Rust 会在编译期抛出异常。