在其他编程语言「go、java」中,都有一套自己的垃圾回收器来保证程序的内存安全。然而,存在垃圾回收器的语言有一个缺点就是垃圾回收会带来一定的开销,最大的问题就是存在 Stop The Word,有些高性能场景是无法接受的。对于 c 和 c++ 来说,不存在垃圾回收的问题,但是需要开发者手动管理内存,增加了开发成本。
Rust 通过所有权系统,保证变量离开作用域后,自用释放内存,即解决了需要垃圾回收器的问题,也解决了开发者手动管理内存问题「本质上还是通过编译器来解决内存管理问题」。当然,带来的问题就是编译时长增加,同时需要理解这一套所有权机制。所有权系统要解决的问题是何时释放内存。
所有权
先介绍一下 Rust 所有权的规则,通过所有权规则来实现对内存的有效管理:
- Rust 中每一个值都有一个被称为所有者的变量
- 值在任意时刻有且只有一个所有者
- 当所有者离开作用域,这个值将被丢弃
Rust 所有权规则本质上就是为了解决如何对堆内存「栈内存无需进行处理,程序运行过程中会进行进栈和出栈操作」进行及时回收的问题?在 Rust 中,String
里的字符串是存储在堆上的。看下面这个 case,print s1
会报错,这是由于值 zjl
的所有权由 s1
转移到 s2
,s1
无法被继续使用。从而保证了值 "zjl"
在任意时刻只有一个所有者。因此,可以得知,Rust 先保证堆内存中值只有一个所有者来解决被重复回收的问题;通过所有者的作用域来解决什么时候回收的问题。
rust
fn main() {
// 分配在堆上,需要进行回收,值 "zjl" 的所有者为 s1
let s1 = String::from("zjl");
// 所有权发生移动, 值的所有者变为 s2
let s2 = s1;
// println!("{}{}", s1, s2); ^^ value borrowed here after move
println!("{}", s2);
// 值的所有者离开作用域,值被丢弃
}
对于标量类型,Rust 直接会 copy
值到栈上,所以下面的代码是可以执行的。Rust 中有一个叫做 Copy trait 的标注,如果一个类型实现了 Copy trait,那么一个旧的变量在赋值给其他变量后仍可以使用。在 Rust 中,实验 Copy trait 的类型有:所有整型、布尔类型、浮点类型、字符类型、元组「当且仅当其包含的类型也都实现了 Copy trait」。
rust
fn main() {
// 值:8, 所有者:s1
let s1 = 8;
// copy 一个值 8 到栈上, 所有者:s2
let s2 = s1;
// 栈中有两个值,对应两个所有者 s1, s2
println!("{}{}", s1, s2);
}
既然对于标量类型,变量赋值会重新在栈上 Copy 一个值。那对于分配到堆上的类型,我们也可以深拷贝的形式,在堆上新分配一块内存,这样就可以保证两个变量分别持有对应的内存块。当然,这样做的缺点就是造成内存的分配和销毁,影响性能。
rust
fn main() {
// 分配在堆上,需要进行回收,值 "zjl" 的所有者为 s1
let s1 = String::from("zjl");
// 所有权发生移动, 值的所有者变为 s2
let s2 = s1.clone();
println!("{}{}", s1, s2);
}
对于函数的入参和出参,同样会发生所有权转移。
rust
fn main() {
let s = String::from("zjl");
take_ownership(s);
// println!("{}", s); s 的所有权已经被转移,无法打印 s
let s = give_ownership(); // 获取函数返回值的所有权
println!("{}", s);
}
// 函数参数赋值也会发生所有权转移, 所有权移动到函数变量 param
fn take_ownership(param: String) {
println!("{}", param);
} // 函数结束,param 移出作用域,调用 drop 方法,回收内存
// 将所有权移交给函数的返回结果上
fn give_ownership() -> String {
let s = String::from("zjl"); // 进入作用域
s // 所有权移交
}
引用和借用
下面是一个计算 String
长度并打印的例子,由于所有权会发生移动,当 String
作为函数入参时,函数调用完成后无法继续使用 String
。这里我们可以 clone
一份 s,但是会造成性能损失,那有没有不 clone
的方案呢?
rust
fn main() {
let s = String::from("hello world");
let len = calculate_len_without_ref(s);
println!("str: {}, len: {}", s, len) // ^ value borrowed here after move, s 的所有权已经移交到 calculate_len_without_ref, 因此这里无法使用
}
fn calculate_len_without_ref(s: String) -> usize {
s.len()
}
在 Rust 中使用 &
即表示引用 ,允许你使用值但不获取其所有权。在下面的代码中,函数 calculate_len
声明了参数类型 &String
,为引用类型。而创建一个引用的行为称为借用 。第三行函数调用时创建一个 s1
的引用,值 "hello word"
的所有者仍然属于 s1,因此可以继续使用 s1。
rust
fn main() {
let s1 = String::from("hello world");
let len = calculate_len(&s1);
println!("str: {}, len: {}", s, len)
}
fn calculate_len(s: &String) -> usize {
s.len()
}
引用又分为可变引用和不可变引用。当需要修改借用过来的变量时,则需要创建一个可变引用。引用的规则为: 任意时刻,要么只有一个可变引用,要么只能有多个不可变应用;引用必须是有效的。
rust
fn main() {
let mut s = String::from("hello world");
// 借用变量 s, 声明一个可变引用 s1
let s1 = &mut s;
// 修改引用的值 s
append(s1);
println!("str: {}", s);
// 声明不可变引用 s2,s3
let (s2, s3) = (&s, &s);
// println!("s1: {}, s2: {}", s1, s2); immutable borrow occurs here, 同时访问了可变引用 s1 和不可变引用 s2, 这是不允许的, 主要是保证数据安全,不允许同时处理可变引用和不可变引用
println!("s2: {}, s3: {}", s2, s3); // 可以编译通过, 已经超出了 s1 的作用域,可以同时处理多个不变引用
// s.push_str("end"); cannot borrow `s` as mutable because it is also borrowed as immutable, 下面访问了不可变引用, 因此不能修改值的所有者 s, 即不能作为 mutable 处理
println!("s: {}, s2: {}", s, s2);
}
所有权解决的是内存安全问题,即保证堆上的每块内存都有对应的所有者,当所有者离开作用域之后,回收内存。引用和借用解决的是数据访问的问题,如何在不破坏所有权规则的条件下,方便快捷的访问变量。基于此,又有了可变引用和不可变引用规则,解决引用变量的可修改问题。