所有权系统(1)

273 阅读6分钟

移动语义 和 复制语义

转移语义:在进行复制或者传参时候,值会从原先的变量绑定 转移到新的变量绑定中。这也称作 所有权的转移,转移后,之前的变量绑定不可用。

复制语义:在进行复制或传入参数的时候,默认进行值复制,而不是转移所有权。

默认情况下,变量绑定都是 移动语义(Move Semantic),例如:

struct Foo;
let x = Foo;
let y = x;
println!("{:?}", x); // error: use of moved value

但是,如果类型实现了 Copy trait,那么该类型就具备了 复制语义(Copy Semantic)。

思考:什么类型可以实现 Copy 呢? 有没有 类型不能实现 Copy 呢?

智能指针 Box<T> 封装了原生指针,是典型的转移语义类型。 Box<T> 无法实现 Copy trait,意味着它被标记为了引用语义,禁止按位复制。 // 为啥无法实现 Copy 呢? 无法为 其他 crate 的 type 实现 其他 crate 的 trait。

基本的原生类型都是值语义,具有值语义的原生类型,在其作为右值进行赋值操作时,编译器会对其进行按位复制。

所有权机制

Rust 中的每个值(分配的内存)都有一个所有者,所有者拥有的权限:

  • 控制资源的释放(不仅是内存)。
  • 出借所有权,包括 不可变的 和 可变的。
  • 转移所有权。

对于实现了(标记) Copy trait 的类型,具备了复制语义,就不存在所有权转移的问题。

默认实现了 Copy trait 的类型有: 基本数字类型,char类型,bool类型。以及由这些类型组成的 数组、元组 和 Option 类型。

绑定、作用域 和 声明周期

可变与不可变

  • 分离可变与不可变性,方便了在适当的场景使用,选用 不用可变引用 或可变引用,或转移所有权。
  • 当函数对变量产生副作用的时候,得明确要求传递的参数得是可变引用,或有所有权。

绑定的时间属性 -- 生存周期

变量绑定具有 “时空” 双重属性。

  • 空间属性 是指标识符与内存空间进行了绑定。
  • 时间属性 是指绑定的时效性,也就是指变量绑定的生存周期。

一个绑定的生存周期,也被称作 生命周期,是和词法作用域相关的。

目前 Rust 的生命周期是基于 词法作用域的,编译器能自动识别函数内部这些局部变量绑定的生命周期。

对于进入新的作用域的变量,也么复制值,要么 转移(或出借)所有权。

作用域的创建时刻

  1. 每个 let 声明都会穿件一个默认的词法作用域,该作用域就是它的声明周期。

    fn demo_01() {
        let a = "hello";  -------------------|
        let b = "rust";   --------------'b  'a 
        let c = "world";  -----------'c 'b  'a
        let d = c;        -------'d  'c 'b  'a
    }                     --------|---|--|---|
    
  2. 花括号: 可以使用花括号在函数体内创建词法作用域。

    fn demo_02() {
        let outer_val = 1;
        let outer_sp = "hello".to_string();
        {
            let inner_val = 2;
            outer_val;
            outer_sp;
        }
    
        println!(" outer_val = {}", outer_val);
        println!(" outer_sp = {}", outer_sp); // value borrowed here after move
    }
    
  3. match 匹配会产生一个新的词法作用域

    fn demo_03() {
        let a = Some("hello".to_string());
        match a {   // can use '&a', then can use 'a' latter
            Some(s) => println!("s = {}", s), // value partially moved here
            _ => println!("nothing"),
        }
        println!("a = {:?}", a); // value borrowed here after partial move
    }
    
  4. 循环语句(for, loop, while)

    fn demo_04() {
        let v = vec![1, 2, 3];
        for i in v {  // v` moved due to this implicit call to `.into_iter()`; 
            // help: consider borrowing to avoid moving into the for loop: `&v`
            println!("i = {}", i);
            println!("v = {:?}", v); // value borrowed here after move
        }
    }
    
  5. if let 和 while let 块

    fn demo_05() {
        let a = Option::Some("hello".to_string());
        if let Some(b) = a {
            // value partially moved here
            // if let Some(b) = a {
            println!("b = {}", b);
        }
        println!("a = {:?}", a); // value borrowed here after partial move
    }
    
    fn demo_06() {
        let mut optional = Some(0);
        while let Some(x) = optional {
            if x > 9 {
                println!("Greater than 9, quit!");
                optional = None;
            } else {
                println!("x = {}", x);
                // 这里 optional 是 Option<i32> 类型,因此是 复制语义。
                println!("optional = {:?}", optional); 
                optional = Some(x + 1);
            }
        }
    }
    
  6. 函数:函数体本身是独立的词法作用域。

    fn main() {
        let str = "hello".to_string();
        demo_06(str);
        println!("str = {}", str); // value borrowed here after move
    }
    
    fn demo_06(s: String) {
        println!("s = {}", s);
    }
    
  7. 闭包:闭包会创建新的作用域,对于环境变量来说有一下三种捕获方式:

  • 对于复制语义类型,以不可变引用 (&T) 来捕获;

  • 对于移动语义类型,执行移动语义 (move) 转移所有权来捕获;

  • 对于可变绑定,如果在闭包中包含对其进行修改的操作,则以可变引用 (&mut) 来捕捉。

    fn demo_07() {
      let s = "hello".to_string();  // s 是String, Move语义,所以按第二条捕获方式,会转移所有权
      let join = |i: &str| s + i;
    
      let s2 = join(" rust");
      println!("after join = {}", s2);
      println!("s = {}", s); // value borrowed here after move
    }
    

所有权借用

直观感受

假设需要写一个函数,用来修改传入的数组的第一个元素,实现方式有两种思路,代码如下:

fn main() {
    let arr1 = [1, 2, 3];
    modify_mut_array1(arr1);
    println!("arr1 = {:?}", arr1); // arr1 = [1, 2, 3]

    let arr1 = modify_mut_array1(arr1);
    println!("arr1 = {:?}", arr1); // arr1 = [11, 2, 3]

    let mut arr2 = [1, 2, 3];
    modify_mut_array2(&mut arr2);
    println!("arr2 = {:?}", arr2); // arr2 = [22, 2, 3]
}

fn modify_mut_array1(mut v: [i32; 3]) -> [i32; 3] {
    v[0] = 11;
    return v;
}

fn modify_mut_array2(v: &mut [i32; 3]) {
    v[0] = 22;
}
  • modify_mut_array1: 传入可变绑定,然后返回修改后的数组。函数参数签名支持模式匹配,相当于使用let将v重新声明成了可变绑定。

  • modify_mut_array2: 传入可变借用,获得借用的可变所有权,直接使用修改即可,函数使用完毕后自动归还,外部继续使用。

引用与借用

引用(Reference) 是 Rust 提供的一种指针语义。

引用与指针的区别在于,指针保存的是其指向内存的地址,而引用可以看作某块内存的别名(Alias),使用引用需要满足编译器的各种安全检查规则。

引用分为 可变引用(&mut) 和 不可变引用(&), 也称作借用(Borrowing),通过 & 或 &mut 操作符来完成所有权租借。

借用所有权会让 所有者(owner),受到如下限制:

  • 在不可变引用借用期间,所有者不能修改资源,并且也不能再进行可变借用。
  • 在可变引用借用期间,所有者不能访问资源,并且也不能再出借所有权。

引用在离开作用域之时,就是其归还所有权之时。

使用 &mut 引用的例子如下:

fn main() {
    let mut v = vec![4, 1, 5, 3, 2];
    bubble_sort(&mut v);
    println!("v = {:?}", v);
}

fn bubble_sort(v: &mut Vec<i32>) {
    let mut n = v.len() - 1;
    while n > 0 {
        for j in 0..n {
            if v[j] > v[j + 1] {
                v.swap(j, j + 1);
            }
        }
        n = n - 1;
    }
}

借用规则

为了保证内存安全,借用必须遵循一下三个规则。

  • 规则一: 借用的声明周期不能长于出借方(拥有所有权的对象)的生命周期。
  • 规则二: 可变借用(引用)不能有别名(Alias),因为可变借用具有独占性。
  • 规则三: 不可变借用(引用)不能再次出借为可变借用。

规则一是为了防止出现悬垂指针,规则二和三核心原则:共享不可变,可变不共享。

关于引用,还需注意一点:解引用操作会获得所有权

在需要对移动语义类型(例如 &String)进行解引用操作时,这种情况,编译器不允许将借用s的所有权转移给append,违反规则三。代码如下所示:

fn join(s: &String) {
    // move occurs because `*s` has type `String`, which does not implement the `Copy` trait
    let append = *s; 
}