rust_3_所有权

所有权系统的设计目标:跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间

所有权规则

  1. Rust 中的每一个值都有一个被称为其 所有者owner)的变量。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

首先:

​ 字符串字面值,属于硬编码,即编译期已经编译进代码段,不会随着程序运行而改变,至于其具体类型,Rust也没说。

let s = "hello";

普通变量的生命周期和作用域和c++类似。遗憾的是这里的s不是普通变量,ta是个slices类型,天生的引用类型,引用的生命周期和c++不一样,所有权章节会遇到。

String类型作为复杂类型的代表,何谓复杂类型,理解上存在非POD类型的数据,或者从c++语义上理解就是需要运行时做额外操作的,比如分配内存,比如初始化虚表。

{
   let s = String::from("hello"); // 从此处起,s 是有效的

   // 使用 s
}                                  // 此作用域已结束,
                                  // s 不再有效

Rust在这一点上像把所有变量变成了unique_ptr对象,只在定义的局部有效,超过作用域就要自动被释放。

s 离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop。这点就像C++的栈上对象离开作用域自动调用析构函数。

变量与数据交互的方式(一):移动

let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); //编译错误
error[E0382]: use of moved value: `s1`
 --> src/main.rs:5:28
  |
3 |     let s2 = s1;
  |         -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value used here after move
  |
  = note: move occurs because `s1` has type `std::string::String`, which does
  not implement the `Copy` trait

其实理解上就是:Rust 永远也不会"自动"创建数据的 “深拷贝”,为此设计了一种新的语义,即一旦需要拷贝,那么,拷贝的源变量将会被标记为一个无效变量,不可再使用,当然也不需要再释放(drop),而其所有权转移到了新的拷贝目标变量上。

这一点,其实为了规避堆上数据重复(深拷贝)的同时又规避double free的一种设计方案,但给编码人员带来的麻烦就是,要明确知道s1是什么类型才能确定后续是否可以继续使用s1。

变量与数据交互的方式(二):克隆

确实 需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的通用函数。

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

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

rust在设计上,从语言语义的角度规避了c++语言使用过程中,可能存在的堆内存使用问题,但也会给出另一种方案来解决堆内存使用的部分场景(比如用户期望深拷贝)。

如果一个类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用。换句话说类型的copy trait注解决定了是否适用这个所有全转移。

如下:

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32)Copy 的,但 (i32, String) 就不是。

基本数据类型变量存在于栈上,默认深拷贝,默认有 Copy trait

Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait,即只要成员或自身自定义了drop析构函数那么这个类型就无法使用copy注解

任何简单标量值的组合可以是 Copy 的,不需要分配内存或某种形式资源的类型是 Copy

整体而言,就是如果一个变量实现深拷贝需要额外操作,那么就不能使用copy注解。copy注解理论上只能用在变量可以完全bytecopy的情形。

但实际上,即使自身涉及到资源的申请和释放,只要不定义drop trait 其自身应该还是可以使用copy trait,但要自己保证资源的合理销毁(比如申请和释放资源通过其他接口,而非构造或者析构来申请或者释放),这是个人理解,待确认。

只在栈上的数据:拷贝

完全支持copy trait,不会发生所有权转移。

所有权与函数

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 ...
                                    // ... 所以到这里不再有效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

返回值与作用域

返回值也可以转移所有权

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值
                                        // 移给 s1

    let s2 = String::from("hello");     // s2 进入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                        // takes_and_gives_back 中,
                                        // 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {             // gives_ownership 将返回值移动给
                                             // 调用它的函数

    let some_string = String::from("hello"); // some_string 进入作用域.

    some_string                              // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

    a_string  // 返回 a_string 并移出给调用的函数
}

引用与借用

我们将获取引用作为函数参数称为 借用borrowing

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

&s1 语法让我们创建一个 指向s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。

这里定义

fn calculate_length(s: &String) -> usize

及调用

let len = calculate_length(&s1);

都需要使用 & 个和c++有点差异,c++调用是时这样传表示s1的地址,即指针。

如果我们尝试修改借用的变量,需要定义可变引用

可变引用

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Rust 可以在编译时就避免数据竞争

可变引用有一个很大的限制:在特定作用域中的特定数据只能有一个可变引用。

let mut s = String::from("hello");
{
    let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
let r2 = &mut s;

也 不能在拥有不可变引用的同时拥有可变引用

let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

println!("{}, {}, and {}", r1, r2, r3);

一个引用的作用域从声明的地方开始一直持续到最后一次使用为止

所以如下代码是可以编译的:

let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用

let r3 = &mut s; // 没问题
println!("{}", r3);

悬垂引用(Dangling References)

fn dangle() -> &String { // dangle 返回一个字符串的引用
    let s = String::from("hello"); // s 是一个新字符串
    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
  // 危险!

所谓的悬垂引用和c++的悬垂指针意义差不多,都是引用或者指向了一个无效数据。

从C++上说即返回了局部变量的引用。

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

我们只能通过不返回引用的方式来返回局部变量,类似于c++的拷贝构造,不过rust中叫所有权转移。结果就是走到 }时不再析构s(调用s.drop)。

引用的规则

让我们概括一下之前对引用的讨论:

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的。
原文地址:https://www.cnblogs.com/kuikuitage/p/14233348.html