Rust笔记 - 所有权

220 阅读4分钟

Rust的所有权是保证了Rust程序能够安全运行的重要机制之一。所有权就是变量拥有资源。所有者可以转移变量的所有权;所有权也可一通过引用来共享,引用机制采用多读者,单写者的方式。与所有权机制紧密关联的还有生存期,相当于变量的作用域,在生存期内变量存活,生存期外,变量自动被销毁,内存被回收。这也是为什么Rust没有垃圾回收器,却能实现类似功能的原因。上面提到的生存期引用垃圾回收机制都与所有权机制密切相关。

那Rust中的所有权机制是怎样的,先看一个例子:

fn main() {
    let s = "hello world".to_owned();
    let take_ownship = s;
    println!("{}", take_ownship);
}

上面就是一个所有权转移的例子。变量s一开始拥有所有权,后面将变量s赋值给变量take_ownship,此时就进行了所有权转移。变量s变成非初始化,即不能够在转移所有权后使用变量s进行任何操作,除非对变量s进行再赋值。使用其他编程语言编码时,也经常对变量的所有权进行转移来保证数据安全,所有权转移机制其实一点都不陌生。

与引用的关系

引用通过指向变量实体来获取和修改变量实体的内容。由于Rust要求数据是安全的的,即没有数据竟争和空指针等其他安全问题。而引用包括可变引用和不可变引用,所以数据可能存在安全风险。因此,Rust要求使用引用时遵循多读者,单写者的规则,保证数据不会竞争。


多读者就是可以有多个不可变引用:

fn main() {
    let s = "hello".to_owned();
    let s1 = &s;
    let s2 = &s;
    println!("s1 = {}, s2 = {}", s1, s2);
}

单写者就是只能有一个可变引用,不能有其他不可变引用或可变引用:

fn main() {
    let mut s = "hello".to_owned();
    let s1 = &s;
    let s2 = &mut s;
    s2.push_str(" world");
    println!("s1 = {}, s2 = {}", s1, s2);
}

上面的代码会编译报错:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> a.rs:4:14
  |
3 |     let s1 = &s;
  |              -- immutable borrow occurs here
4 |     let s2 = &mut s;
  |              ^^^^^^ mutable borrow occurs here
5 |     s2.push_str(" world");
6 |     println!("s1 = {}, s2 = {}", s1, s2);
  |                                  -- immutable borrow later used here

error: aborting due to previous error

因为上面同时存在不变引用和可变引用,违反了单写者规则。正确的做法是:

fn main() {
    let mut s = "hello".to_owned();
    let s1 = &s;
    println!("s1 = {}", s1);
    
    let s2 = &mut s;
    s2.push_str(" world");
    println!("s2 = {}", s2);
}

上面讲了可变引用与不可变引用的关系。下面讲一下所有权引用的关系:

  • 引用是依附于所有权上。即,引用生存期不能超过所有权实体生存期
  • 引用无法转移所有权

与垃圾回收机制的关系

Rust的垃圾回收机制与Go/Java的自动垃圾回收机制完全不同,倒有点像C++的RAII机制。Rust的垃圾回收机制依赖于变量的生存期生存期到了,变量自动被销毁(调用变量关联的drop trait)。而变量的生存期与变量的作用域所有权转移密切相关。

  • 如果一个变量离开作用域,变量的生存期就到了。但如果变量将所有权转移到另外一个变量(作用域范围更大)则资源生存期得以“延续”。

  • 如果将一个变量的所有权转移到容器内,如:vector,map等。资源的生存期跟随容器生存期

  • 如果变量实现了CloneCopy trait,则说明该变量的资源是可复制的。Rust并不会使用所有权转移机制,而是直接采用赋值方式。这是所有权转移机制的例外。为什么会有这个例外呢?因为赋值的成本很低,编码上也更方便。下面看一个例子:

    #[derive(Copy, Clone, Debug)]
    struct Num {
        a: u32,
        b: u32,
    }
    
    fn main() {
        let n1 = Num { a: 1, b: 2 };
        let n2 = n1;
        println!("n = {:?}, n2 = {:?}", n1, n2);
    }
    

    上面的代码如果使用所有权转移机制,编译会报错,但实际上能够正常编译运行。在Rust中integer,boolean,char等基础类型都实现了CloneCopy trait。