1. 简介
所有运行的程序都必须管理其使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时不断地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。
2. 所有权规则
- Rust 中每一个值都有一个被称为「所有者」的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者离开作用域,这个值将被丢弃(内存回收)。
2.1 作用域
变量的作用域是其在程序中有效的范围,一个变量作用域从声明的地方开始一直持续到最后一次使用为止,且其作用域被限制在变量声明所处的最内层 {}
代码块中,即最大不能超出其所处的最内层 {}
区域。
2.2 垃圾回收
- 在有垃圾回收(garbage collector,GC)的语言中, GC 记录并清除不再使用的内存,而我们并不需要关心它。
- 在没有 GC 的语言中,需要手动识别出不再使用的内存并调用代码显式释放,跟请求内存的时候一样。
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。
3. 变量与数据交互方式
以 Rust 标准库中的 String
数据类型为例。一个 String
类型的变量实际由三部分组成:
- 一个指向存放字符串内容内存的指针字段
- 一个长度字段
- 一个容量字段
以上三部分都是存储在栈上,而由指针指向的字符串是存储在堆上的。
3.1 移动
- 对于在栈上的变量,本质上并没有移动的概念。
- 对于在堆上的变量,比如
String
,当将一个String
变量赋值给另一个String
变量时,拷贝的只是存储在栈上的内容:
let s1 = String::from("hello");
let s2 = s1;
因此,两个数据指针同时指向同一个堆内存位置,即同一个堆内存地址有两个所有者,这样会导致在垃圾回收时出现「二次释放」的错误。即当 s1
和 s2
离开作用域是都将尝试释放同一块堆内存。
- 为了确保内存安全,与其尝试拷贝被分配的内存,Rust 则认为
s1
不再有效,因此 Rust 不需要在s1
离开作用域后清理任何东西。 - 当将
s1
赋值给s2
后,s1
就失效了,往后对它的访问都会引发错误。 - 这样看起来将
s1
赋值给s2
不是「拷贝」,而是「移动」。
【注】「将值传递给函数」以及「将值从函数返回」在语义上与给变量赋值相似。
3.2 克隆
- 对于栈上的变量,将一个变量赋值给另一个变量即为克隆。
- 对于堆上的变量,将一个变量赋值给另一个变量实为移动,如果确实需要赋值
s1
中堆上的数据,而不仅仅是栈上的数据,可以使用clone
函数来实现克隆。克隆后的两个String
变量指向的堆内存是不相同的,因此都是有效的:
let s1 = String::from("hello");
let s2 = s1.clone();
println!("x = {}, y = {}", x, y);
【注】当出现 clone
调用时,一些特定的代码被执行而且这些代码可能相当消耗资源。
4. 引用
如果我们想将一个 String
变量传给调用函数,并在调用函数后仍然能够使用该 String
:
- 一种方式是将该
String
作为函数返回值的一部分,但这过于繁琐。 - 另一种方式就是使用不获取值的所有权的「引用」。
在 Rust 中,使用 &
来获取一个变量的引用。变量的引用允许使用值但不获得其所有权。
- 引用可以看作是一种特殊的变量,其有效作用域和普通变量一样。
- 当引用离开作用域后并不丢弃它指向的数据,因为它没有指向的数据的所有权。
- 正如变量默认是不可变的,引用也一样,(默认)不允许修改引用的值。
- 若需要创建可变引用,首先需要将原
String
变量设为mut
,然后使用&mut
创建可变引用。
let mut s = String::from("hello");
let r = &mut s;
- 可变引用有一个很大的限制:在特定作用域中的特定数据只能有一个可变引用,而且也不能在拥有不可变引用的同时拥有可变引用。
- 一个引用的作用域从声明的地方开始一直持续到最后一次使用为止,因此在最后一次使用不可变引用后是可以声明可变引用的(因为它们的作用域没有重叠)。
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 包含最后一个索引,可以不写两个点号之后的值。
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
是一个不可变引用。
// 读取字符串第一个单词
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);
}