Rust从入门到摔门而出- rust的内存管理模型中的 “值” 所有权
Rust 的内存管理模型以其安全性和性能著称,主要通过所有权系统、借用(borrowing)和生命周期(lifetimes)来实现。本章说明和总结下所有权系统,如有不足欢迎大家补充和说明。
所有权系统
-
所有权(Ownership):每个值在任意时刻都有一个所有者(Owner),即某个变量。一旦所有者超出作用域,该值会被自动删除,从而避免内存泄漏。
-
移动(Move):当变量赋值给另一个变量时,所有权会转移(Move)给新变量,旧变量不再拥有该值,从而防止多次释放同一内存。这种机制避免了悬空指针(Dangling Pointer)和二次释放(Double Free)。
-
借用(Borrowing):允许多个变量临时借用值的引用,分为可变借用和不可变借用。在一个时间点上,不能同时存在多个可变借用或一个可变借用和一个不可变借用。这保证了数据竞争不会发生。
所有权
在Rust中,每个值都有一个变量作为其所有者。所有者变量负责管理该值的内存
1. Rust 中的每一个值都有一个 所有者(owner)。
2. 值在任一时刻有且只有一个所有者。
3. 当所有者(变量)离开作用域,这个值将被丢弃。
fn main() {
let s1 = String::from("Hello Rust!");
let s2 = s1; // s1的所有权被转移给s2,s1不再有效
// println!("1.{}", s1); // 编译错误,s1不再有效
println!("2.{}", s2); // 打印 2.Hello Rust!
}
在上述代码中,s1 的所有权被转移给了 s2,因此在 s2 赋值后,s1 变得无效。
说明:所有权在任一时刻只能存在一个: 同一个值在任何时刻只能有一个所有者,这意味着在某一时刻只能有一个变量拥有该值。
所有权移动
fn main() {
let s1 = String::from("Hello Rust!");
{
let s2 = s1;
println!(1. {}", s2);
} // 这里 s2 超出作用域并被释放
// println!("2: {}", s1); // 编译错误,因为 s1 已经无效
}
在
{}中s1的所有权被转移给了s2, 意味着s2现在是该String的新所有者。s1在此之后变得无效,不能再被使用。 当离开{}时,s2超出其作用域时,Rust 自动调用drop函数释放s2所拥有的资源,即String类型实例所占用的堆内存。
在函数中所有权也会被转移
fn main() {
let s1 = String::from("Hello Rust!");
take_and_give_back(s1); // s1 的所有权被转移到函数,
// println!("{}", s1); // 编译错误,因为 s1 已经转移函数中
}
fn take_and_give_back(s: String) -> String {
println!("{}", s); // 打印 Hello Rust!
} // 离开函数 超出作用域并 被释放
在 Rust 中 一旦所有权从一个变量转移到另一个变量,原变量将不再拥有该值,并且不能再直接使用原变量来访问该值。所有权不会自动“回到”原变量。
下面示例:所有权转移后再返回
fn main() {
let s1 = String::from("Hello Rust!");
let s1 = take_and_give_back(s1); // s1 的所有权被转移到函数,并在函数中返回
println!("{}", s1); // s1 重新获得所有权,可以继续使用
}
fn take_and_give_back(s: String) -> String {
s // 返回传入的字符串,所有权回到调用者
}
通过函数调用可以实现所有权的转移和返回,但这并不是真正的“回到最初”,而是通过显式的所有权转移来实现
借用(Borrowing)
借用允许多个变量临时访问一个值而不转移所有权,分为不可变借用和可变借:
不可变借用 & 和可变借用 &mut. Rust 能够让多个变量访问相同的数据,而无需转移所有权。这在编译时通过借用检查器进行验证,确保数据的安全访问。
- 不可变借用:多个不可变借用是允许的,但在有不可变借用时不能有可变借用。
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2); // 允许多个不可变借用
}
- 可变借用:同一时刻只能有一个可变借用,且不能同时存在不可变借用。
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // 编译错误,不允许同时有多个可变借用
println!("{}", r1);
}
下面代码 正确使用了 Rust 的借用规则,在每次可变借用之前确保了先前的可变借用已经结束,因此它能够正确编译和运行。
fn main() {
let mut s1 = String::from("Hello Rust!");
{
let s2 = &mut s1;
println!("1.{}", s2); // 输出 1.Hello Rust!
} // s2 在这里离开了作用域,释放了对 s1 的可变借用
let s3 = &mut s1;
println!("2.{}", s3); // 输出 2.Hello Rust!
}
生命周期(Lifetimes)
Rust 的生命周期(Lifetimes)是其内存安全机制中的一个关键概念,用于管理引用的有效性和防止悬垂引用。生命周期通过编译时的静态分析,确保引用在其持有的数据的生命周期内有效,从而避免了常见的内存错误。下面详细说明 Rust 的生命周期及其原理和作用。
生命周期的基本概念
- 生命周期(Lifetimes):
- 生命周期是一个引用保持有效的时间范围(存在时间创建到回收时间)。
- 每个引用都有一个生命周期,表示引用可以合法使用的范围。
示例:
fn main(){
let s1 = String::from("我是S1");
{//
let s2 = String::from("我是S1");
println!("1.{}", s2);
}//s2 的生命周期至此销毁结束
println!("2.{}", s1);
}//s1 的生命周期至此销毁结束
- 悬垂引用(Dangling References):
- 悬垂引用是指引用超出了其指向的数据的生命周期,从而引用了无效的数据。Rust 的生命周期机制可以防止这种情况发生。
示例:
// ❌ 编译错误
fn main(){
let s1;
{
let s2 = String::from("我是 S2");
s1 = &s2 // 返回对局部变量 s2 的引用
}//s2 的生命周期至此销毁结束
println!("2.{}", s1); //
}
为了避免悬垂引用,可以使用所有权转移:
fn main(){
let s1;
{
let s2 = String::from("我是 S2");
s1 = s2; // 所有权转移
}
println!("2.{}", s1); // 输出 我是 S2
}
悬垂引用是指引用指向的对象已经被销毁,导致引用指向无效的内存。在Rust中,编译器通过借用检查器严格禁止这种情况,确保引用的安全性。通过理解和遵循Rust的所有权和借用规则,可以有效地避免悬垂引用的问题。
生命周期标注
生命周期标注使用撇号 (') 语法来指定引用的生命周期。例如,'a 是一个生命周期标识符。
fn example<'a>(x: &'a str) -> &'a str {
x
}
在这个例子中,函数 example 接受一个引用 x并返回一个具有相同生命周期的引用。生命周期标注 'a表示参数和返回值具有相同的生命周期。
生命周期的原理
Rust 编译器通过静态分析在编译时检查生命周期,确保引用的安全性。生命周期的主要原理包括:
-
生命周期的推断:
- 在大多数情况下,Rust 编译器能够自动推断生命周期,而不需要显式标注。
- 只有在复杂的引用关系中,才需要显式标注生命周期。
-
生命周期的规则:
- 函数参数的生命周期与返回值的生命周期有关联。
- 生命周期标注仅用于表示引用之间的相对关系,不会改变引用的实际生命周期。
生命周期的作用
生命周期的主要作用是确保内存安全,防止悬垂引用和数据竞争。具体作用包括:
1.防止悬垂引用:
- 确保引用在其持有的数据的生命周期内有效,避免引用超出数据的作用。
2.数据竞争的防止:
- 通过静态分析,确保引用在多线程环境下的安全访问,防止数据竞争
3.内存安全的保证:
- Rust 的生命周期机制确保所有引用在有效范围内使用,从而避免了内存访问错误。
生命周期的实际应用
简单的生命周期:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
在这个例子中,函数 longest 接受两个字符串引用,并返回其中一个更长的字符串引用。生命周期标注 'a 表示参数和返回值共享相同的生命周期。
结构体中的生命周期 结构体中的引用也需要生命周期标注,以确保结构体实例在引用数据的有效范围内使用。
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
println!("{}", i.part);
}
在这个例子中,ImportantExcerpt 结构体包含一个引用 part,其生命周期 'a 确保结构体实例在 part 引用的数据的生命周期内有效。
生命周期省略规则
在某些情况下,Rust 编译器可以自动推断生命周期,省略显式标注。以下是常见的生命周期省略规则:
- 每个引用参数都有一个独立的生命周期。
- 如果只有一个引用参数,编译器会推断返回值的生命周期与该参数相同。
- 如果有多个引用参数且返回值引用其中一个参数,编译器无法确定时,需要显式标注。
fn first_word(s: &str) -> &str {
&s[..s.find(' ').unwrap_or(s.len())]
}
在这个例子中,编译器根据省略规则推断出 s 和返回值具有相同的生命周期。
生命周期示例:复杂情况
struct Book<'a> {
title: &'a str,
author: &'a str,
}
impl<'a> Book<'a> {
fn get_title(&self) -> &str {
self.title
}
fn get_author(&self) -> &str {
self.author
}
}
fn main() {
let book = Book {
title: "Rust Programming",
author: "Steve",
};
println!("Title: {}", book.get_title());
println!("Author: {}", book.get_author());
}
在这个示例中,Book 结构体和其方法 get_title 和 get_author 都使用了生命周期标注 'a,确保结构体实例和引用数据的生命周期一致。
生命周期总结
Rust 的生命周期机制通过编译时的静态分析,确保引用在有效范围内使用,从而避免悬垂引用和数据竞争。生命周期标注用于表示引用之间的相对关系,不改变实际生命周期。通过推断和省略规则,Rust 编译器在大多数情况下能够自动处理生命周期,确保代码的内存安全和高效执行。