ZEVORN.blog

September 30, 2024

Rust 基本知识

noteLearningOS3.2 min to read

什么是所有权

所有权(ownership)是 Rust 用于管理内存的一组规则,通过在编译阶段检查程序内存使用的合法性,来避免运行时的内存安全问题。

所有权的优势:

  1. 运行时零开销,不会影响性能;

个人认为的缺点:

  1. 开发者的学习成本稍高,并随着程序的复杂度上升,需要付出更多的心智成本。

所有权原则

所有权的基本规则:

  1. Rust 中每一个值都被一个变量所拥有,该变量被称之为:值的所有者(owner);
  2. 一个值都是只能被一个变量所拥有,或者说,一个值只能拥有一个所有者;
  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(自动调用 drop)。

理解 Rust 中的堆栈,可以加深对所有权的理解。

变量绑定背后的数据交互

  1. 对于基本类型(存储在栈上), Rust 会自动拷贝;
  2. 否则不能自动拷贝(比如 String 类型)。

Rust 避免二次释放的策略: 对于复杂类型(由存储在栈中的堆指针、其他成员共同组成),比如 String,当发生所有权转移时,旧所有者将失效,因此不会在离开作用域时 drop。

Rust 移动(move): 区别于其他语言的浅拷贝(shallow copy),所有权发生移动后,旧所有者失效了。

Rust 克隆(clone): 首先,Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。

如果我们确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的方法。

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

这段代码能够正常运行,说明 s2 确实完整的复制了 s1 的数据。

Rust 拷贝(浅拷贝): 浅拷贝只发生在栈上,因此性能很高,在日常编程中,浅拷贝无处不在。

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

但这段代码似乎与我们刚刚学到的内容相矛盾:没有调用 clone,不过依然实现了类似深拷贝的效果 —— 没有报所有权的错误。

原因是像整型这样的基本类型在编译时是已知大小的,会被存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效(x、y 都仍然有效)。换句话说,这里没有深浅拷贝的区别,因此这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它(可以理解成在栈上做了深拷贝)。

Rust Copy : Rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。

那么什么类型是可 Copy 的呢?可以查看给定类型的文档来确认,这里可以给出一个通用的规则:

任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy 的。

如下是一些可以 Copy 的类型:

  1. 所有整数类型,比如 u32;
  2. 布尔类型,bool,它的值是 true 和 false;
  3. 所有浮点数类型,比如 f64;
  4. 字符类型,char;
  5. 元组,当且仅当其包含的类型,也都是 Copy 的时候,比如 (i32, i32)
  6. 不可变引用 &T,注意:可变引用 &mut T 不可以 Copy。

函数传值与返回

将值传递给函数,一样会发生 移动 或者 复制,就跟 let 语句一样,下面的代码展示了所有权、作用域的规则:

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 移出作用域。不会有特殊操作

你可以尝试在 takes_ownership 之后,再使用 s,看看如何报错?例如添加一行 println!("在move进函数后继续使用s: ",s);。

同样的,函数返回值也有所有权,例如:

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 并移出给调用的函数}

所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦: 总是把一个值传来传去来使用它。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题。