Skip to content

所有权

解决的问题

  • 跟踪代码的哪些部分正在使用 heap 的哪些数据
  • 最小化 heap 上的重复数据
  • 清理 heap 上未使用的数据以避免空间不足

所有权规则

  • 每个值都有一个变量,这个变量是该值的所有者(owner);
  • 每个值同时只能有一个所有者;
  • 当所有者超出作用域(scope)时,该值将被删除。
作用域
fn main() {
    let x = {               // x可用
        let a = 10;         // a 可用
        println!("{}", a);  // 可以对 a 进行相关操作

    }; // a 作用域到此结束,a不再可用,但 x 依然可用

    // x 依然可用,可以对 x 进行相关操作
}// x 作用域到此结束,不再可用

所有权变动

所有权变动 会发生在 2 种情况下:

  1. 把一个值赋给另一个变量时;

    fn main() {
        let a = String::from("Hello");  // 此时 a 拥有了字符串 Hello 这个值的所有权
        let b = a;                      // 此时 b 拥有了字符串 Hello 这个值的所有权,而 a 失去了所有权
        // 即字符串的所有权从 a 移动到了 b
    
        println!("{}", a);  // 会报错
    }
    

  2. 函数传参和返回

    fn main() {
        let s = String::from("Hello");  // s 拥有字符串 Hello 的所有权
        str_size(s);                    // 传参后字符串 Hello 的所有权就移动到了函数的参数上
    
        // 到此处 s 已经失去字符串 Hello 的所有权了
        println!("{}", s);  // 会报错
    }
    
    fn str_size(arg :String) -> usize {    // 字符串 Hello 的所有权被移动到了 arg 上
        let size = arg.len();
        return size;
    }
    
    fn main() {
        let s1 = String::from("Hello"); // s1 拥有字符串 Hello 的所有权
        let s2 = add_str(s1);           // 传参后字符串 Hello 的所有权就移动到了函数的参数上
        // 函数返回后 s2 获得了字符串长度的所有权
    }
    
    fn str_size(arg :String) -> usize {    // 字符串 Hello 的所有权被移动到了 arg 上
        let size = arg.len();
        return size;    // 字符串长度这个值从 size 转移到了返回值上
    }
    

所有权变动分为 2 种情况: 移动拷贝

所有的变量都是栈上数据的一个别名,简单标量类型数据保存在栈上,复杂类型则在栈上保存一份数据结构,实际的数据存放在堆上。

对于所有权的变动,主要是在栈到堆上的区别。

  1. 移动 Move

    默认的变动行为是:移动 Move。所有权被移动后,原来的变量就不能再使用了。

    即所有权被移动后,原先的变量在栈上的数据结构就变回未初始化的状态。

    fn main() {
        let a = String::from("Hello");
        let b = a;
    
        // 字符串 Hello 的所有权已经转移到了 b,此时不能再操作 a 了
        println!("{}", a);  // borrow of moved value: `a` value borrowed here after move
    }
    

  2. 拷贝 Copy

    另外一种变动行为是:拷贝 Copy。所有权不会被转移,而是把值拷贝了一份。

    其实就是新的变量和原有的变量,在栈上的这两份数据结构,她们指向了同一份堆上的数据。

    fn main() {
        let a = 5;
        let b = a;  // 这里 b 只是拷贝了一份 5,a 依然有所有权
    
        // 标量类型默认实现了 Copy trait,所以其所有权变动行为是 Copy
        println!("{}, {}", a b);  // a 依然有所有权,可以继续操作
    }
    
  3. 克隆 Clone

    克隆需要显式的实现 Clone trait,并且显式的调用 .clone() 方法。

    克隆不仅栈上有新的变量和原有变量两份数据,而且堆上的数据也是各自一份。

    克隆不会导致原有变量失去所有权。因为克隆是实实在在复制了一份。


  • 如果类型没有实现 Copy trait,则所有权发生转移时执行的是移动 Move
  • 如果类型实现了 Copy trait,则所有权发生转移时执行的是复制 Copy(浅拷贝)
  • 如果类型实现了 Clone trait,则显示调用 .Clone() 会复制一份堆内存(深拷贝)
  • 实现了 Clone trait,在变量离开作用域时会自动调用 Drop 函数
  • 实现了 Copy trait 就不能实现 Drop trait.

关于 Copy:

  • 任何简单标量的组合类型都可以是 Copy 的
  • 任何需要分配内存或某种资源的都不是 Copy 的
  • 一些拥有 Copy trait 的类型:
    • 所有整数类型,例如 i32, u64等
    • 所有浮点类型,例如 f32, f64
    • bool
    • char
    • Tuple(前提是Tuple中的类型都是 Copy 的),例如 (i32, f64)
    • 引用,例如 let p1 = &s; let p2 = p1; p1 是一个引用,赋值给 p2 的时候执行的是 Copy。

Note

如果没实现 Copy trait,则默认为 Move。如果实现了 Copy trait,则默认为 Copy。

实现了 Copy 就不能实现 Drop,实现了Clone 就可以实现 Drop。

简单标量类型及其组合默认实现了 Copy trait,默认分配在栈上。

借用

在其他语言中,函数传参是将值复制了一份传递过去,源数据依然可以继续使用。

但是 Rust 的所有权概念非常严格,传了参就失去了所有权,想要再次获取还得函数返回出来。

所以为了方便,在 Rust 中就有了「借用」的概念。

  • 引用 Reference& 符号表示引用,允许你引用某些值而不取得其所有权。(引用默认情况下也不可变)
  • 借用 Borrow :把引用作为函数参数的这个行为,就叫做借用。
  • 借用分为 共享借用(不可变引用) 和 独享借用(可变引用)。

共享借用(不可变引用)

fn main() {
    let s = String::from("Hello");
    let l = getLen(&s); // &符号表示 引用 s
    println!("{}'s length is {}", s, l);
}

fn getLen(s: &String) -> usize {    // 这里借用了 s
    s.len()
}

独享借用(可变引用)

fn main() {
    let mut s = String::from("Hello");      // 首先源数据得是可变的,得加上 mut
    let l = getLen(&mut s);                 // 然后在引用的时候也需要声明为可变引用,不然函数里依然不能修改
    println!("{}'s length is {}", s, l);
}

fn getLen(s: &mut String) -> usize {    // 最后形参也得对应上可变
    s.push_str(", world")
    s.len()
}

要注意一个数据不可以同时被可变引用(独享借用)两次,这是为了防止数据竞争。

fn main() {
    let mut s = String::from("Hello");
    let r1 = &mut s;
    let r2 = &mut s;    // 这里就报错了,不可以在同一作用域内进行多份可变借用
    println!("{}'s length is {}", s, l);
}
但是可以非同时的,或者说非同一作用域内进行多份可变引用
fn main() {
    let mut s = String::from("Hello");
    {
        let r1 = &mut s;
    }
    let r2 = &mut s;

    // r1 和 r2 不在同一作用域,所以不冲突。r1 在离开作用域就会被清理
}