rust 所有权

2022/10/31 16:49 下午 posted in  rust

所有权可以说是Rust中最为独特的一个功能了。正是所有权概念和相关工具的引入,Rust才能够在没有垃圾回收机制的前提下保障内存安全。因此,正确地了解所有权概念及其在Rust中的实现方式,对于所有Rust开发者来讲都是十分重要的。在本章中,我们会详细地讨论所有权及其相关功能:借用、切片,以及Rust在内存中布局数据的方式。

所有权就是指一个东西归属谁。Rust 中一个变量对应一个值,变量就称为这个值的所有者。

lex x = 5;

这句话的意思就是,5这个数字所在的内存块的所有者是 x。

所有权规则

  1. Rust 中的每一个值都有一个对应的变量作为它的所有者
  2. 在同一时间内,值有且仅有一个所有者
  3. 当所有者离开自己的作用域时,它持有的值就会被释放掉

变量和数据交互的方式:移动

把一个变量赋值给另一个变量

fn main() {
    let x = 5;
    let y = x;
    println!("x = {}, y = {}", x, y);


    let s1 = String::from("hello");
    let s2 = s1;
    println!("s1 = {}, s2 = {}", s1, s2);
}

这段代码的作用就是 把一个变量赋值给另一个变量。

整数是已知固定大小的简单值,x y 两个值 5 会同时被推入到当前的栈中。

然而,将 s1 赋值给 s2 时,便复制了一次 String 的数据,这意味着我们复制了它存储在栈上的指针、长度及容量字段,没有复制指针指向的堆数据。此时的内存布局应该是这样:

2022-10-31-16.48.47

为了确保内存安全,避免复制分配的内存,Rust 在这种场景下回简单的将 s1 废弃,不再视其为一个有效的变量。
2022-10-31-17.03.02

也就解释了这段代码执行为什么报错

error[E0382]: borrow of moved value: `s1`
  --> src/main.rs:11:34
   |
8  |     let s1 = String::from("hello");
   |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
9  |     let s2 = s1;
   |              -- value moved here
10 |
11 |     println!("s1 = {}, s2 = {}", s1, s2);
   |                                  ^^ value borrowed here after move
   |
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

总的来说 移动(move) 这种变量和数据交互方式,仅仅是将栈上的数据移动了,堆数据还是同一个,并且将移动前的变量 s1 设置为失效,意味着 s1 和堆数据之间的绑定关系已经不存在了(相当于一个野指针?)。所以移动后在访问 s1 是不被允许的。

变量和数据交互的方式:克隆

let mut s1 = String::from("hello");
// let s2 = s1;
let s2 = s1.clone();

s1.push_str(", world");
println!("s1 = {}, s2 = {}", s1, s2);

克隆 = 深度 copy 堆上的数据。s1、s2 是两个独立的变量,指向的堆数据也不是同一个,所以在 clone 后对 s1 进行修改,s2 还是不变的。

trait :Copy 、Drop

当一种类型拥有了 Copy 这种 trait,那么它的变量就可以在赋值给其他变量之后保持可用性。

如果一种类型实现了 Drop,那么 Rust 就不允许它再实现 Copy。

所有权与函数

将值传递给函数在语义上类似于对变量进行赋值,会触发移动或者复制,就像赋值语句一样。

fn main() {
    let x = 5;
    show(x);
    println!("x = {}", x);

    let s1 = String::from("hello");
    show_str(s1);
    println!("函数外部 s1 = {}", s1)
}

fn show(x: i32) {
    println!("函数内部 x = {}", x)
}

fn show_str(s: String) {
    println!("函数内部 s1 = {}", s)
}

string 类型变量当做参数传递给函数后,变量的所有权会移动到函数的作用域内,main 函数中就不在持有当前变量的所有权了,所以当调用 show_str 之后在访问 s1就会报错。

从另一个方面来理解,函数执行结束后会销毁变量对应的内存空间,然后在 main 函数中访问一个已经移除调的内存空间报错也是合情合理的。

赋值并不是唯一涉及移动的操作。值在作为参数传递或从函数返回时也会被移动。

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:8:30
  |
6 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
7 |     show_str(s1);
  |              -- value moved here
8 |     println!("函数内部 s1 = {}", s1)
  |                                  ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

引用与借用

如果我们想在函数结束后还想要使用变量,可以将变量的引用传递到参数中。

引用允许在不获取所有权的情况下使用值。

fn main() {
    let x = 5;
    show(x);
    println!("x = {}", x);

    let s1 = String::from("hello");
    show_str(&s1);
    println!("函数外部 s1 = {}", s1)
}

fn show(x: i32) {
    println!("函数内部 x = {}", x)
}

fn show_str(s: &String) {
    println!("函数内部 s1 = {}", s)
}

修改点

  1. show_str(&s1);传递参数,传的是 s1 的引用
  2. fn show_str(s: &String) 修改为 String 的引用类型

2022-10-31-17.38.00

&s1,指在不转移所有权的前提下,创建一个指向 s1 值的引用,引用不持有 s1 的所有权,所以在函数结束时,原 s1 仍然可用。
函数签名中的 & 用来表名参数 s 的类型是一个引用。s 并不持有指向值的所有权,在函数结束时不会销毁指向值。

通过引用传递参数给函数的方法也被称为借用。

可变引用

fn main() {
    let mut s1 = String::from("hello");
    show_str(&mut s1);
    println!("函数外部 s1 = {}", s1)
}

fn show_str(s: &mut String) {
    println!("函数内部 s1 = {}", s);
    s.push_str(", world");
}

mut 表示可变的,变量使用 mut,引用使用 &mut。
通过 mut 关键字将变量或者引用声明为可变的。
可以在函数内部修改其引用变量的值。
一个变量可以有多个不可变引用,但是只能有一个可变引用
一个变量不能同时有不可变引用和可变引用。

悬垂引用

这类指针指向曾经存在某处内存地址,但该内存已经被释放掉甚至被重新分配另作他用了。

这不就是野指针?

rust 语言中 ,编译器会 确保引用永远不会进入这种悬垂状态。

假如当前持有某一个数据的引用,编译器保证这个数据不会在引用被销毁前离开自己的作用域。

创造一个悬垂引用的例子:

fn main() {
    let x = dangle();
    println!("x = {}", x)
}

fn dangle() -> &String {
    let s = String::from("hello");
    return &s;
}

dangle 函数,返回 String 的引用,函数内部,声明了一个字符串 s,返回给调用者 s 的引用,看着貌似没什么问题。

由于变量 s 创建在函数内,所以它会在函数执行完毕时随之释放,但是我们的代码依旧返回指向 s 的引用,这个引用指向的是一个无效的内存地址。

error[E0106]: missing lifetime specifier
 --> src/main.rs:6:16
  |
6 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
6 | fn dangle() -> &'static String {
  |                 +++++++

切片

切片也是不持有所有权的数据类型。
切片:集合中某一段连续的元素序列,而不是整个集合。

字符串字面量就是切片。

总结

所有权是 Rust 区别于其他编程语言的一个重要部分。
变量称为其对应的内存块的所有者,变量拥有所有权。

所有权发生移动的几个方式

  1. 变量赋值给其他变量
  2. 变量当做参数传递到函数
  3. 函数返回值赋值给变量

作用域可以简单理解为{},大括号内部称为一个作用域。
当变量离开作用域时,变量会自动释放其所有权。

所有权只会发生在堆上分配的数据,栈上分配的数据没有所有权的概念。