程序管理运行时计算机内存的方式:
- 具有垃圾回收机制
- 程序员自己分配与释放内存
- Rust 通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。
背景:栈与堆 入栈比在堆上分配内存要快,因为入栈时分配器无需为存储新数据去搜索内存空间;其位置总是栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。
所有权系统要处理的问题:
- Rust 中的每一个值都有一个被称为其所有者的变量
- 值在任一时刻有且只有一个所有者
-
变量作用域
{ // s 在这里无效,它尚未声明
let s = "hello"; // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,s 不再有效
这里有两个重要的时间点:
- 当 s 进入作用域时,它就是有效的;
-
内存与分配
以 Rust String 类型为例,为了支持一个可变、可增长的文本片段,需要在 堆 上分配一块在编译时未知大小的内存来存放内容。这意味着:
必须在运行时向内存分配器(memory allocator)请求内存。
- 需要一个当我们处理完 String 时将内存返回给分配器的方法。
第一部分由 String::from
完成,它的实现请求其所需的内存。
第二部分在 Rust 是内存在拥有它的变量离开作用域后就被自动释放,当 s 离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 }
处自动调用 drop。
{
let s = String::from("hello"); // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,
// s 不再有效
变量与数据交互的方式(一):移动
let s1 = String::from("hello");
let s2 = s1;
当 s2 和 s1 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
为了确保内存安全,在 let s2 = s1
之后,Rust 认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
这段代码会无法编译,因为 s1 不再生效。
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error
如果你在其他语言中听说过术语 浅拷贝(shallow copy)和 深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move),而不是浅拷贝。
上图是 s1 无效之后的内存表现。
变量与数据交互的方式(二):克隆
如果我们确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的通用函数。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
只在栈上的数据:拷贝
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它。
Rust 有一个叫做 Copy trait 的特殊注解,如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。支持 Copy 的类型:
- 所有整数类型,比如 u32。
- 布尔类型,bool,它的值是 true 和 false。
- 所有浮点数类型,比如 f64。
- 字符类型,char。
元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。
所有权与函数
```rust 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 移出作用域。没有特殊之处
<a name="i7rOU"></a>
## 返回值与作用域
```rust
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("yours"); // 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 并移出给调用的函数
}
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。
如果我们想要函数使用一个值但不获取所有权该怎么办呢?如果我们还要接着使用它的话,每次都传进去再返回来就有点烦人了,除此之外,我们也可能想返回函数体中产生的一些数据。
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的长度
(s, length)
}
Rust 对此提供了一个不用获取所有权就可以使用值的功能,叫做 引用(references)。