rust 所有权

85 阅读8分钟

基本规则

  1. rust中的每一个值都有一个变量, 称为其所有者
  2. 一次只能有一个所有者
  3. 当所有者的不在程序范围时, 改值将被删除

所有权

实例化一个类型并且将其绑定到变量名上将会创建一些内存资源, 而这些内存资源将会在其整个生命周期中被rust 编译器检验。 被绑定的变量即为该资源的所有者。

基于域的资源管理

Rust 将使用资源最后被使用的位置或者一个函数域的结束来作为资源被析构和释放的地方。 这种概念被称为drop丢弃。

  • rust中没有垃圾回收机制
  • 在C++中, 这被称为 RAII

这个通俗的来讲, 就是当一个变量从某一行开始,再也没有使用过那么Rust 编译器就会自动释放。 在这儿简单的判断一下一个变量的作用域 一个变量被制造出来那一步,到不再使用它的那一步,就是这个变量的域。 但是也可以通过 {} 显式的确定。 后面还有一种就是生命周期标注。

释放是分级进行的

删除一个结构体时, 结构体本身会被先释放,紧接着才分别释放相应的子结构体的并以此类推。

  • 每个内存资源仅会被释放一次

移动所有权

rustc: borrow of moved value: foo value borrowed here after move

这个 Rust 编译器错误信息的意思是:“在移动操作之后,尝试借用了一个已经被移动的值 foo。”

具体来说,这个错误发生在你试图访问一个已经被移动(move)到另一个变量或数据结构的值。在 Rust 中,由于所有权(ownership)规则,一个值在任何给定时间只能有一个所有者。当一个值被移动到另一个变量时,原来的变量就失去了对它的所有权,因此不能再被借用或访问。

这里是一个简单的例子来说明这个错误:

fn main() {
    let foo = vec![1, 2, 3];
    let bar = foo; // `foo` 被移动到 `bar`
    println!("{:?}", foo); // 尝试在这里借用 `foo`,但 `foo` 已经被移动了
}

在这个例子中,foo 被移动给了 bar,之后尝试打印 foo 的内容时就会触发这个错误,因为 foo 已经不再有效。

归还所有权

所有权也可以从一个函数中被归还

struct Foo {
    x: i32,
}

fn do_something() -> Foo {
    Foo { x: 42 }
    // 所有权被移出
}

fn main() {
    let foo = do_something();
    // foo 成为了所有者
    // foo 在函数域结尾被 dropped 释放
}

使用引用借用所有权

如果不想转移所有权,就可以通过引借用的方式。引用允许我们通过& 操作符来借用对一个资源的访问权限。引用也会如同其他资源一样被释放。

struct Foo {
    x: i32,
}

fn main() {
    let foo = Foo { x: 42 };
    let f = &foo;
    println!("{}", f.x);
    // f 在这里被 dropped 释放
    // foo 在这里被 dropped 释放
}

& 这种使用不可改变被引用的值, 要想改变则需要使用 &mut

注意:

一个作用域: 指的是一个变量的生命周期,即创建到释放的过程

  • 不可变引用在一个作用域内可以不限制数量,这是因为不可变引用相当于是在读一个文件,那么不论多少个任务去读,都是一致的。
  • 可变引用在一个作用域内只能有一个。 这是因为可变引用相当于在写一个文件,那么如果有多个任务在同时写,就会导致潜在的数据争用。
  • 不可变引用和可变引用不能混用。可以想一下, 我是一个不可变引用,就是希望我引用的数据是不会发生变化的,但是,在我工作的范围内,居然有另一个人在篡改。 这明显是不对的。
fn main(){
    let foo = Foo { x: 42 };
    // 创建了f变量,f的作用域开始
    let f = &foo;
    // 不再使用f,f的作用域结束
    // 创建了f1变量, f1的作用域开始
    let f1 = &mut foo;
    f1.x = 12;
    // 不在使用f1, f1的作用域结束
}

多个可变引用

fn main(){
    let foo = Foo { x: 42 };
    // 创建了f变量,f的作用域开始
    let f = &foo;
    // 删掉: 不再使用f,f的作用域结束
    // 创建了f1变量, f1的作用域开始
    let f1 = &mut foo;
    // 将f的作用域结束放到这儿. 就会报错, cannot mutate immutable variable `foo`
    println!("{:?}", f1);
    f1.x = 12;
    // 不在使用f1, f1的作用域结束
}

上面的作用域判断,不一定在低版本生效。 应该在1.70 之后才会自动判断。 这儿存疑。

解引用

使用 &mut 引用时, 你可以通过 * 操作符来修改其指向的值。 你也可以使用 * 操作符来对所拥有的值进行拷贝(前提是该值可以被拷贝——我们将会在后续章节中讨论可拷贝类型)。

fn main() {
    let mut foo = 42;
    let f = &mut foo;
    let bar = *f; // 取得所有者值的拷贝
    *f = 13;      // 设置引用所有者的值
    println!("{}", bar);
    println!("{}", foo);
}

传递借用的数据

Rust 对于引用的规则也许最好用以下的方式总结:

  • Rust 只允许同时存在一个可变引用或者多个不可变引用,不允许可变引用和不可变引用同时存在
  • 一个引用永远也不会比它的所有者存活得更久。

而在函数间进行引用的传递时,以上这些通常都不会成为问题。

内存细节:

  • 上面的第一条规则避免了数据争用的出现。什么是数据争用?在对数据进行读取的时候,数据争用可能会因为同时存在对数据的写入而产生不同步。这一点往往会出现在多线程编程中。
  • 而第二条引用规则则避免了通过引用而错误的访问到不存在的数据(在 C 语言中被称之为悬垂指针)。

引用的引用

多重引用, rust 会自动解引用

显示生命周期

生命周期,rust编译器会自动标注,但有时候,rust编译器无法判断生命周期的时候,就需要手动标注了。

单个生命周期

struct Foo {
    x: i32,
}

// 参数 foo 和返回值共享同一生命周期
fn do_something<'a>(foo: &'a Foo) -> &'a i32 {
    return &foo.x;
}

fn main() {
    let mut foo = Foo { x: 42 };
    let x = &mut foo.x;
    *x = 13;
    // x 在这里被 dropped 释放从而允许我们再创建一个不可变引用
    let y = do_something(&foo);
    println!("{}", y);
    // y 在这里被 dropped 释放
    // foo 在这里被 dropped 释放
}

多个生命周期

struct Foo {
    x: i32,
}

// foo_b 和返回值共享同一生命周期
// foo_a 则拥有另一个不相关联的生命周期
fn do_something<'a, 'b>(foo_a: &'a Foo, foo_b: &'b Foo) -> &'b i32 {
    println!("{}", foo_a.x);
    println!("{}", foo_b.x);
    return &foo_b.x;
}

fn main() {
    let foo_a = Foo { x: 42 };
    let foo_b = Foo { x: 12 };
    let x = do_something(&foo_a, &foo_b);
    // foo_a 在这里被 dropped 释放因为只有 foo_b 的生命周期在此之后还在延续
    println!("{}", x);
    // x 在这里被 dropped 释放
    // foo_b 在这里被 dropped 释放
}

静态生命周期

一个静态变量是一个在编译期间即被创建并存在于整个程序始末的内存资源。他们必须被明确指定类型。 一个静态生命周期是指一段内存资源无限期地延续到程序结束。需要注意的一点是,在此定义之下,一些静态生命周期的资源也可以在运行时被创建。 拥有静态生命周期的资源会拥有一个特殊的生命周期注解 'static。 'static 资源永远也不会被 drop 释放。 如果静态生命周期资源包含了引用,那么这些引用的生命周期也一定是 'static 的。(任何缺少了此注解的引用都不会达到同样长的存活时间)

static PI: f64 = 3.1415;

fn main() {
    // 静态变量的范围也可以被限制在一个函数内
    static mut SECRET: &'static str = "swordfish";

    // 字符串字面值拥有 'static 生命周期
    let msg: &'static str = "Hello World!";
    let p: &'static f64 = &PI;
    println!("{} {}", msg, p);

    // 你可以打破一些规则,但是必须是显式地
    unsafe {
        // 我们可以修改 SECRET 到一个字符串字面值因为其同样是 'static 的
        SECRET = "abracadabra";
        println!("{}", SECRET);
    }
}

数据类型中的生命周期

struct Foo<'a> {
    i:&'a i32
}

fn main() {
    let x = 42;
    let foo = Foo {
        i: &x
    };
    println!("{}",foo.i);
}

生命周期的标注有时候很复杂,我也不大清楚。

总结

通过所有权,我们可以避免以下情况:

  1. 无意将对资源的修改
  2. 忘记及时地释放资源
  3. 资源意外的地被释放两次
  4. 在资源释放后使用了它
  5. 由于读取数据的同时有其他人正在向资源中写入数据的而引起的数据争用
  6. 在编译器无法做担保时, 清晰看到代码的作用域