rust 所有权

Posted by 无限可能的想象力 on February 21, 2022

所有权

根据内存管理方式的不同,主流的编程语言可以划分为:

  • 安全优先:由垃圾收集器来管理内存,比如Java、Python、Golang
  • 控制优先:由开发者负责释放内存,比如C、C++

Rust旨在同时保证安全和性能。Rust以这种方式来解决这个问题:严格限制程序使用指针的方式。

这些规则为Rust实现安全的并发编程奠定了基础。使用Rust的线程原语,保证内存安全的规则可以用于保证避免数据竞争。

所有权

在Rust中,所有权是内置在语言中的,并通过编译期检查确保强制执行。每个值都只有一个决定它生命周期的所有者。当所有者被释放,它拥有的值也会被删掉。

所有者和它们拥有的值组成一棵树。 每个变量都是一棵树;当这个变量离开作用域时,销毁整棵树。

Rust在一下方面扩展了所有权树的想法:

  • 值可以从一个所有者移动到另一个所有者。允许修改所有权树
  • 简单类型如整数、浮点数和字符被排除在所有权规则以外。它们被称为Copy类型
  • 标准库提供了引用计数的指针类型Rc和Arc,它们允许一个值有多个所有者
  • 可以借用一个值的引用,引用是生命周期受限的非占有的指针

move

对于move操作,源对象放弃了值的所有权,把所有权转移给了目的对象,同时源 对象变为未初始化的状态;此时目的对象控制值的生命周期。Rust 程序一次一个值、一次一个 move 的构建和拆除复杂的结构。

Rust在几乎所有场景下都使用move操作。

fn test_move() {
    struct Person {
        name: String,
        birth: i32,
    }

    let mut composers = Vec::new();
    composers.push(Person {
        name: "test".to_string(),
        birth: 22,
    });
    println!("{}", composers.len())
}

在上述代码中,move 发生的场景如下:

  • 从函数返回值
  • 构造新的值
  • 向函数传值

Copy类型

赋予一个Copy类型的值会拷贝它,而不是移动它。源对象仍然保持初始化状态和可用性,它的值不会发生改变。向函数和构造器传递Copy类型的变量也类似。

标准的 Copy 类型包括所有的机器整数和浮点数类型、char 和 bool 类型,以及少数其他类型。Copy 类型的元组或者固定大小的数组也是 Copy 类型。

只有简单的逐位拷贝的类型才可以是Copy类型。

用户自定义类型只是默认不是 Copy 类型。如果你的结构体的所有字段都是 Copy 类型, 那么你可以通过在定义上方加上属性 #[derive(Copy, Clone)] 来把它变为Copy类型。

在Rust中,所有的移动都是逐字节的浅拷贝,同时把源对象设置为未初始化。

Rust 的原则之一就是开销对程序员来说必须是明显的。基本的操作必须保持简单。

Rc和Arc:共享所有权

Rc 和 Arc 类型非常相似,它们唯一的不同之处在于: Arc(原子引用计数) 可以直接安全的在线程之间共享;Rc 则使用更快一些的非线程安全代码来更新引用计数。如果你不需要在线程之间共享指针,那就没有必要承担 Arc 的性能损失,所以你应该使用 Rc;Rust 会阻止你无意间在线程之间传递 Rc。

对于任意类型T,一个Rc<T>值是一个指向在堆上分配的T类型值的指针,同时还附有一个引用计数。克隆一个 Rc<T> 类型的值并不意味着拷贝T,它只是简单的创建另一个指向它的指针,并且递增引用计数。

拥有三个引用的引用计数字符串内存分布如图所示:

一个 Rc 指针拥有的值是不可变的。

Rust的内存和线程安全保证依赖于没有值既是共享的又是可变的。Rust 假设 Rc 指针指向的值要被共享,因此它必须是不可变的。

引用

引用绝不应该比它们指向的值生命周期更长。为了强调这一点,Rust将创建某个值的引用称为借用值。

引用可以访问一个值,同时不影响它的所有权。 引用有两种:

  • 共享引用:只能读取不能修改被引用的值。共享引用是Copy类型
  • 可变引用:可以读取和修改被引用的值。可变引用不是Copy类型

将共享引用和可变引用完全分离是内存安全的基础。

以值传参数;以引用传参数

使用引用

引用允许函数在不获取所有权的情况下访问或者操作一个数据结构。

#[test]
fn test_reference() {
    let x = 10;
    let r = &x;
    assert_eq!(*r, 10);

    let mut y = 2;
    // 创建可变引用
    let m = &mut y;
    *m += 30;
    assert_eq!(*m, 32);
}

引用可以进行比较,注意比较操作符的类型必须完全相同

引用永不为空。

Rust有两种类型的胖指针:

  • 切片的引用是一种胖指针,包括切片的起始地址和它的长度。
  • trait 对象,一个实现了特定 trait 的值的引用。一个 trait 对象包含值的地址和一个指向该值对 trait 的实现的指针,用于调用 trait 的方法。

引用安全

一个生命周期是一个引用可以安全使用的程序区间:可能是一条语句、一个表达式、也可能是一些变量的作用域,或者类似的区间。生命周期完全是 Rust 的编译期视图。在运行时引用只是一个地址,生命周期是它的类型的一部分,并没有运行时表示。

引用的两种约束:

  • 变量的生命周期必须包含或包括它的引用的生命周期。
  • 如果你把引用存储到变量 r 中,引用的生命周期必须覆盖变量的整个生命周期:从它初始化到最后一次使用它。

第一种约束限制了引用生命周期的上限,第二张约束限制了它的下限。

如下图所示,这样的生命周期是不存在的,违反了引用的两种约束。

在 Rust 中,一个函数的签名总是暴露出函数体的行为。

函数签名中的生命周期让 Rust 能获得传进函数的引用和函数返回的引用的关系,然后保证它们都被安全地使用。

当一个引用类型出现在其他类型的定义中时,你必须写出它的生命周期:

struct S {
    r: &'static i32
}

总结

移动和引用计数指针是两种缓解所有权树过于死板的方法。