Rust所有权

2022-03-18 16:51:31 浏览数 (1)

1. 简介

所有运行的程序都必须管理其使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时不断地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。

2. 所有权规则

  • Rust 中每一个值都有一个被称为「所有者」的变量。
  • 值在任一时刻有且只有一个所有者。
  • 当所有者离开作用域,这个值将被丢弃(内存回收)。

2.1 作用域

变量的作用域是其在程序中有效的范围,一个变量作用域从声明的地方开始一直持续到最后一次使用为止,且其作用域被限制在变量声明所处的最内层 {} 代码块中,即最大不能超出其所处的最内层 {} 区域。

2.2 垃圾回收

  • 在有垃圾回收(garbage collector,GC)的语言中, GC 记录并清除不再使用的内存,而我们并不需要关心它。
  • 在没有 GC 的语言中,需要手动识别出不再使用的内存并调用代码显式释放,跟请求内存的时候一样。

Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。

3. 变量与数据交互方式

以 Rust 标准库中的 String 数据类型为例。一个 String 类型的变量实际由三部分组成:

  • 一个指向存放字符串内容内存的指针字段
  • 一个长度字段
  • 一个容量字段

以上三部分都是存储在栈上,而由指针指向的字符串是存储在堆上的。

3.1 移动

  • 对于在栈上的变量,本质上并没有移动的概念。
  • 对于在堆上的变量,比如 String,当将一个 String 变量赋值给另一个 String 变量时,拷贝的只是存储在栈上的内容:
代码语言:javascript复制
let s1 = String::from("hello");
let s2 = s1;

因此,两个数据指针同时指向同一个堆内存位置,即同一个堆内存地址有两个所有者,这样会导致在垃圾回收时出现「二次释放」的错误。即当 s1s2 离开作用域是都将尝试释放同一块堆内存。

  • 为了确保内存安全,与其尝试拷贝被分配的内存,Rust 则认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。
  • 当将 s1 赋值给 s2 后,s1 就失效了,往后对它的访问都会引发错误。
  • 这样看起来将 s1 赋值给 s2 不是「拷贝」,而是「移动」。

【注】「将值传递给函数」以及「将值从函数返回」在语义上与给变量赋值相似。

3.2 克隆

  • 对于栈上的变量,将一个变量赋值给另一个变量即为克隆。
  • 对于堆上的变量,将一个变量赋值给另一个变量实为移动,如果确实需要赋值 s1 中堆上的数据,而不仅仅是栈上的数据,可以使用 clone 函数来实现克隆。克隆后的两个 String 变量指向的堆内存是不相同的,因此都是有效的:
代码语言:javascript复制
let s1 = String::from("hello");
let s2 = s1.clone();

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

【注】当出现 clone 调用时,一些特定的代码被执行而且这些代码可能相当消耗资源。

4. 引用

如果我们想将一个 String 变量传给调用函数,并在调用函数后仍然能够使用该 String

  • 一种方式是将该 String 作为函数返回值的一部分,但这过于繁琐。
  • 另一种方式就是使用不获取值的所有权的「引用」。

在 Rust 中,使用 & 来获取一个变量的引用。变量的引用允许使用值但不获得其所有权。

  • 引用可以看作是一种特殊的变量,其有效作用域和普通变量一样。
  • 当引用离开作用域后并不丢弃它指向的数据,因为它没有指向的数据的所有权。
  • 正如变量默认是不可变的,引用也一样,(默认)不允许修改引用的值。
  • 若需要创建可变引用,首先需要将原 String 变量设为 mut,然后使用 &mut 创建可变引用。
代码语言:javascript复制
let mut s = String::from("hello");
let r = &mut s;
  • 可变引用有一个很大的限制:在特定作用域中的特定数据只能有一个可变引用,而且也不能在拥有不可变引用的同时拥有可变引用。
  • 一个引用的作用域从声明的地方开始一直持续到最后一次使用为止,因此在最后一次使用不可变引用后是可以声明可变引用的(因为它们的作用域没有重叠)。
代码语言:javascript复制
let mut s = String::from("hello");

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

let r3 = &mut s; // 没问题
println!("{}", r3);
  • 编译器会确保指向 String 的引用持续有效。

【注】在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用;而且在作用域内引用必须总是有效的。

在 Rust 中,将获取「引用」作为函数参数称为「借用」。

5. Slice

Slice 是一种特殊的引用,它允许你引用集合中一段连续的元素序列,而不用引用整个集合。可以使用一个由中括号中的 [starting_index..ending_index] 指定的 range 创建一个 Slice:

  • starting_index 是 Slice 的第一个位置,ending_index 则是 Slice 最后一个位置的后一个值。
  • 在其内部,Slice 的数据结构存储了 Slice 的开始位置和长度,长度对应于 ending_index 减去 starting_index 的值。
  • 如果 Slice 包含第一个索引(0),可以不写两个点号之前的值。
  • 如果 Slice 包含最后一个索引,可以不写两个点号之后的值。
代码语言:javascript复制
let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

了解了 Slice,就可以正确地理解「字符串字面值」:字符串字面值本质就是 Slice。

代码语言:javascript复制
let s = "Hello, world!";

其中,s 的类型是 &str,它是一个执行二进制程序特定位置的 Slice。这也就是为什么字符串字面值是不可变的,因为 &str 是一个不可变引用。

代码语言:javascript复制
// 读取字符串第一个单词
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
fn main() {
    let my_string = String::from("hello world");

    // first_word 中传入 `String` 的 Slice
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word 中传入字符串字面值的 Slice
    let word = first_word(&my_string_literal[..]);

    // 因为字符串字面值就是字符串 Slice,
    // 这样写也可以,即不使用 Slice 语法!
    let word = first_word(my_string_literal);
}

0 人点赞