Rust 笔记Rust 语言中的 所有权 与 生命周期
1. 所有权基本概念
1.1 理解所有权
所有权是 Rust 语言的核心概念,它在内存管理方面发挥着重要作用。在本节中,我们将讨论所有权的重要性以及它在 Rust 中的应用。
1.1.1 所有权在内存管理中的重要性
所有权系统使 Rust 能够在不使用垃圾收集器的情况下实现内存安全。通过所有权规则,Rust 能够在编译时检测潜在的内存问题,如悬垂指针和数据竞争,从而确保内存安全和高性能。
1.1.2 三个所有权规则
在 Rust 中,所有权遵循以下三个规则:
序号 | 规则描述 |
1 | 每个值 都有一个变量 作为其 所有者。 |
2 | 同一时间 只能有一个 所有者。 |
3 | 当所有者离开作用域时,值将被释放。 |
1.2 变量、值 和 所有权
在本节中,我们将讨论变量、值以及它们在所有权系统中的关系。
1.2.1 变量和值之间的关系
在 Rust 中,变量绑定到值,而值存储在内存中。变量是值的所有者,并负责管理值的生命周期。当一个变量离开作用域时,其绑定的值将被释放。
1.2.2 变量作用域与所有权
变量的作用域是从变量声明开始到其所在的代码块结束。当变量离开作用域时,Rust 会自动释放其绑定的值。这种自动内存管理方式避免了悬垂指针和内存泄漏等问题。
1.3 数据交互 与 所有权
数据在 Rust 中可以通过三种方式进行交互:移动、克隆和复制。以下是这三种交互方式及其对所有权的影响:
交互方式 | 描述 |
移动 | 当一个值从一个变量移动到另一个变量时,原始变量将失去对值的所有权。这种情况下,值的所有权被转移,而不是复制。 |
克隆 | 克隆是 创建值的深拷贝。当一个值被克隆时,新的变量将拥有新值的所有权,而原始变量仍然拥有原始值的所有权。 |
复制 | 对于实现 Copy trait 的类型,当值从一个变量赋值给另一个变量时,值会被复制,而所有权保持不变。这意味着复制后,两个变量都可以独立地使用值。 |
1.3.2 示例代码与描述
fn main() { // 创建一个字符串 let s1 = String::from("hello"); // 使用 `=` 将 s1 移动到 s2 let s2 = s1; // 此时 s1 不再有效,因为所有权已经转移给了 s2 // println!("s1: {}", s1); // 这一行会导致编译错误 // 克隆 let s3 = s2.clone(); // 使用 clone 方法创建一个新的字符串,与 s2 相同但具有不同的所有权 println!("s2: {}", s2); // s2 仍然有效 println!("s3: {}", s3); // s3 有效,因为它是 s2 的克隆 // 复制 let x = 5; let y = x; // 对于基本类型,如整数,这里发生的是复制,而不是移动 println!("x: {}", x); // x 仍然有效 println!("y: {}", y); // y 有效,因为它是 x 的副本 }
在这个代码示例中,我们展示了数据在 Rust 中的三种交互方式:移动、克隆和复制:
- 移动:当我们将 s1 移动到 s2 时,s1 失去了对值的所有权,因此不能再使用。这种情况下,值的所有权被转移,而不是复制。
- 克隆:我们使用 clone 方法创建一个新的字符串 s3,它与 s2 相同但具有不同的所有权。这样,s2 和 s3 都可以使用。
- 复制:对于基本类型(如整数),当我们将 x 赋值给 y 时,发生的是复制而不是移动。这意味着 x 和 y 都有自己的值,可以独立使用。
1.3.3 关于 Copy
trait 和 Clone
trait
1. Copy
trait
pub trait Copy: Clone { }
当一个类型实现了 Copy trait 时,它的值可以在赋值、传参和返回值时自动复制。实现 Copy trait 的类型同时也必须实现 Clone trait,因为 Clone 是 Copy 的超类。
一些简单的类型,如整数、浮点数和布尔值都实现了 Copy trait。通常,只有在类型的实例可以安全地按位复制时,才应该实现 Copy trait。这意味着类型不能包含任何需要析构的资源,如引用计数的智能指针或文件句柄等。
#[derive(Copy, Clone)] struct Point { x: i32, y: i32, } fn main() { let p1 = Point { x: 1, y: 2 }; let p2 = p1; // p1 的值被复制到 p2 println!("p1: ({}, {})", p1.x, p1.y); println!("p2: ({}, {})", p2.x, p2.y); }
2. Clone
trait
Clone trait 用于显式地创建类型值的副本。与 Copy trait 不同,实现 Clone trait 的类型可能需要执行深拷贝操作,例如分配新的内存。一般来说,实现 Clone trait 的类型可以包含需要析构的资源,如引用计数的智能指针或文件句柄等。
#[derive(Clone)] struct MyStruct { data: Vec<i32>, } impl Clone for MyStruct { fn clone(&self) -> MyStruct { MyStruct { data: self.data.clone(), } } } fn main() { let s1 = MyStruct { data: vec![1, 2, 3], }; let s2 = s1.clone(); // 显式地创建 s1 的副本 println!("s1: {:?}", s1.data); println!("s2: {:?}", s2.data); }
1.4 总结
在第一章中,我们已经介绍了 Rust 所有权的基本概念,包括所有权的重要性、变量与值之间的关系以及数据交互与所有权的关系。在接下来的章节中,我们将深入讨论引用、借用以及生命周期等概念。
2. 引用与借用
2.1 引用的概念
引用是一种让你访问值而无需获取其所有权的方式。
2.1.1 引用的目的
引用的主要目的是允许我们在不改变所有权的情况下访问和操作数据。
2.1.2 使用引用
为了创建一个引用,我们需要使用 &
符号。例如:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } // 定义一个函数,接受一个字符串的引用作为参数 fn calculate_length(s: &String) -> usize { s.len(); // 通过引用访问字符串的长度 }
在这个示例中,我们创建了一个名为 s1 的字符串,并将其引用传递给 calculate_length 函数。这样,我们可以在不改变 s1 所有权的情况下获取字符串的长度。
fn main() { let s1 = String::from("hello"); let s2 = &s1; // 创建一个指向 s1 的引用 println!("s1: {}, s2: {}", s1, s2); }
在这个示例中,我们创建了一个名为 s1 的字符串,并创建了一个名为 s2 的引用,它指向 s1。请注意,在打印 s1 和 s2 时,它们的值是相同的,因为 s2 是指向 s1 的引用。
fn main() { let mut x = 5; let y = &mut x; // 创建一个指向 x 的可变引用 *y += 1; // 通过解引用操作符修改引用指向的值 println!("x: {}, y: {}", x, y); }
在这个示例中,我们创建了一个名为 x 的可变整数,并创建了一个名为 y 的可变引用,它指向 x。我们可以通过解引用操作符 * 修改引用指向的值。请注意,在打印 x 和 y 时,它们的值是不同的,因为我们修改了 y 指向的值,而不是 x 本身。
2.2 可变引用与不可变引用
Rust 中可以创建可变引用和不可变引用。这两种引用类型遵循一定的规则,以确保内存安全。
2.2.1 理解可变引用
可变引用是指可以修改其指向的值的引用。为了创建一个可变引用,我们需要使用 &mut
符号。以下是一个使用可变引用的示例:
fn main() { let mut s = String::from("hello"); change(&mut s); println!("The modified string is '{}'.", s); } // 定义一个函数,接受一个字符串的可变引用作为参数 fn change(s: &mut String) { // 通过可变引用修改字符串 s.push_str(", world"); }
在这个示例中,我们创建了一个名为 s 的可变字符串,并将其可变引用传递给 change 函数。这样,我们可以在不改变 s 所有权的情况下修改字符串。
请注意,当我们创建一个可变引用时,原始数据也必须是可变的。例如,如果我们尝试创建一个不可变字符串的可变引用,编译器将报错。
fn main() { let s = String::from("hello"); // 不可变字符串 let s_mut_ref = &mut s; // 尝试创建可变引用 }
在这个示例中,我们尝试为一个不可变字符串创建一个可变引用,这将导致编译错误,因为原始数据(s
)不是可变的。
希望这些示例能帮助你更好地理解可变引用的概念。在后续章节中,我们将讨论不可变引用以及引用规则和借用的概念。
2.2.2 理解不可变引用
不可变引用是指不能修改其指向的值的引用。当你创建一个引用时,默认情况下它是不可变的。以下是一个使用不可变引用的示例:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
在这个示例中,我们创建了一个名为 s1 的字符串,并将其不可变引用传递给 calculate_length 函数。这样,我们可以在不改变 s1 所有权的情况下获取字符串的长度。
【Tips】可变引用与不可变引用的规则
在 Rust 中,可变引用和不可变引用有以下规则:
- 同一时间,要么有一个可变引用,要么有多个不可变引用。
- 引用必须始终有效。
这些规则有助于防止数据竞争和悬垂指针等问题。
2.3 借用
2.3.1 借用的概念
借用是 Rust 中一种使用引用传递数据的方式,它遵循所有权规则,确保内存安全。借用的主要目的是允许我们在不改变所有权的情况下访问和操作数据,同时遵循所有权规则以确保内存安全。
2.3.2 使用借用
借用是通过创建引用来实现的。以下是一个使用借用的简单示例:
fn main() { let mut s = String::from("hello"); let s1 = &s; // 不可变借用 let s2 = &s; // 不可变借用 let s3 = &mut s; // 可变借用 }
在这个示例中,我们创建了一个名为 s 的可变字符串,并创建了两个不可变借用 s1 和 s2,以及一个可变借用 s3。请注意,根据可变引用和不可变引用的规则,这段代码会导致编译错误,因为我们不能在同一时间既有可变借用又有不可变借用。
现在我们已经介绍了 Rust 中引用与借用的基本概念,包括引用的概念、可变引用与不可变引用以及借用的概念。
3. 生命周期
本章讨论 Rust 中的 生命周期 概念,它在保证引用安全方面发挥着重要作用。
3.1 生命周期的概念
生命周期是一个编译时的概念,用于确保 Rust 程序中的引用始终有效。生命周期的主要目的是防止 悬垂引用1(dangling reference)的产生。
3.1.1 生命周期的目的
生命周期的目的是 确保引用始终指向有效的内存。通过在编译时检查生命周期,Rust 能够确保运行时不会发生 悬垂引用 等内存安全问题。例如:
fn main() { let r: &i32; { let x = 42; r = &x; } // 编译错误:x 已经离开作用域,因此 r 指向的引用已失效 println!("r: {}", r); }
在这个例子中,我们试图创建一个指向局部变量 x 的引用 r。然而,当 x 的作用域结束时,r 仍然试图访问它。这将导致悬垂引用,但 Rust 编译器会阻止这种情况发生,产生以下编译错误:
error[E0597]: `x` does not live long enough --> src\main.rs:5:13 | 4 | let x = 42; | - binding `x` declared here 5 | r = &x; | ^^ borrowed value does not live long enough 6 | } | - `x` dropped here while still borrowed 7 | // 编译错误:x 已经离开作用域,因此 r 指向的引用已失效 8 | println!("r: {}", r); | - borrow later used here
这个错误是因为尝试在 x
离开作用域后使用它的引用。在这个例子中,x
是在一个代码块中声明的,所以它的生命周期只在这个代码块内。当尝试在代码块外部使用 r
时,编译器会报错,因为 x
的生命周期已经结束,r
指向的引用已失效。这是 Rust 的所有权系统保证内存安全的一部分。可以看到 Rust 编译器通过检查生命周期来确保引用安全,防止 悬垂引用 的产生。
3.1.2 生命周期的工作原理
生命周期是通过在函数签名中使用生命周期参数来表示的。生命周期参数以单引号 ’ 开头,后跟一个标识符。以下是一个使用生命周期参数的示例:
// 定义一个函数,接受两个字符串切片作为参数,并返回一个字符串切片 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
在这个示例中,我们定义了一个名为 longest
的函数,它接受两个字符串切片作为参数,并返回一个字符串切片。生命周期参数 'a
表示输入参数 x
和 y
的生命周期以及返回值的生命周期。这意味着返回值的生命周期与输入参数中较短的生命周期相同。
以下是一个使用 longest
函数的示例:
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is '{}'.", result); }
输出结果为:
The longest string is 'abcd'.
在这个示例中,我们创建了两个字符串 string1
和 string2
,并调用 longest
函数找出较长的字符串。请注意,string1
是一个 String 类型,而 string2
是一个字符串字面值。我们使用 as_str()
方法将 string1
转换为字符串切片,以便与 string2
类型相匹配。
通过使用生命周期参数,Rust 编译器能确保 longest
函数返回的引用始终有效。在本示例中,result
的生命周期与 string1
和 string2
中较短的生命周期相同。
在后续章节中,我们将讨论生命周期省略规则以及结构体和方法中的生命周期。
3.2 生命周期注解
生命周期注解是一种显式地指定引用生命周期的方法,以便编译器能够正确地分析代码。在本节中,我们将讨论生命周期注解的语法和用法。
3.2.1 生命周期注解的语法
生命周期注解以单引号('
)开头,后跟一个标识符。生命周期参数通常在函数签名中使用,用于表示输入参数和返回值的生命周期。例如:
// 定义一个函数,接受两个字符串切片作为参数,并返回一个字符串切片 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
在这个示例中,我们定义了一个名为 longest
的函数,它接受两个字符串切片作为参数并返回一个字符串切片。生命周期参数 'a 表示输入参数 x
和 y
的生命周期以及返回值的生命周期。这意味着返回值的生命周期与输入参数中较短的生命周期相同。
3.2.2 使用生命周期注解
以下是一个使用生命周期注解的示例:
// 定义一个结构体,包含一个字符串切片的引用 struct Excerpt<'a> { part: &'a str, } fn main() { let novel = String::from("叫我Jack。你好!"); let first_sentence = novel.split('。').next().expect("找不到'.'"); let i = Excerpt { part: first_sentence }; println!("Excerpt: '{}'", i.part); }
输出:
Excerpt: '叫我Jack'
在这个示例中,我们定义了一个名为 Excerpt
的结构体,它包含一个字符串切片的引用。我们在结构体定义中使用生命周期参数 'a
来表示引用的生命周期。这意味着 Excerpt
结构体中的 part
字段引用的字符串切片必须在整个结构体的生命周期内保持有效。
在 main
函数中,我们创建了一个名为 novel
的字符串,并使用 split
方法找到第一个句子。然后,我们创建了一个 Excerpt 结构体实例 i
,并将 first_sentence
作为 part
字段的值。请注意,i
的生命周期与 novel
的生命周期相同,因为它引用了 novel
中的字符串切片。
3.3 静态生命周期
静态生命周期是 Rust 中一种特殊的生命周期,用于表示引用的生命周期与整个程序的生命周期相同。在本节中,我们将讨论静态生命周期的概念。
3.3.1 理解静态生命周期
在 Rust 中,静态生命周期使用 'static
表示。具有静态生命周期的引用可以在整个程序的生命周期内保持有效。静态生命周期通常用于全局变量和静态变量,因为这些变量在整个程序的生命周期内都是有效的。
3.3.2 静态生命周期 示例
示例 1:静态字符串
// 静态字符串具有静态生命周期 static GREETING: &'static str = "Hello, world!"; fn main() { println!("{}", GREETING); }
输出:
Hello, world!
在这个示例中,我们定义了一个静态字符串 GREETING
,它具有静态生命周期。这意味着 GREETING
变量在整个程序的生命周期内都是有效的。
示例 2:静态变量
// 定义一个具有静态生命周期的静态变量 static COUNT: &'static i32 = &66; fn main() { println!("答案是: {}", *COUNT); }
输出:
答案是: 66
在这个示例中,我们定义了一个具有静态生命周期的静态变量 COUNT
。这意味着 COUNT
变量在整个程序的生命周期内都是有效的。
示例 3:结构体中的静态生命周期
// 定义一个结构体,包含一个具有静态生命周期的字符串切片 struct Message<'a> { content: &'a str, } // 定义一个具有静态生命周期的字符串 static TEXT: &'static str = "Hello, world!"; fn main() { // 创建一个 Message 结构体实例,使用具有静态生命周期的字符串 let msg = Message { content: TEXT }; println!("消息: {}", msg.content); }
输出:
消息: Hello, world!
在这个示例中,我们定义了一个名为 Message
的结构体,它包含一个具有静态生命周期的字符串切片。我们还定义了一个具有静态生命周期的静态字符串 TEXT
。然后,我们创建了一个 Message
结构体实例,并将 TEXT
作为 content
字段的值。请注意,msg
的生命周期与整个程序的生命周期相同,因为它引用了一个具有静态生命周期的字符串。
3.4 生命周期省略规则
在 Rust 中,生命周期参数通常需要在函数签名中显式指定。然而,在某些情况下,编译器可以自动推断生命周期参数,从而允许我们省略它们。这些情况遵循一组称为“生命周期省略规则”的规则。在本节中,我们将讨论生命周期省略规则的定义及其应用。
3.4.1 生命周期省略规则的定义
生命周期省略规则是 Rust 编译器用来自动推断生命周期参数的一组规则。这些规则包括:
- 每个引用的参数都有自己的唯一生命周期参数。换句话说,一个函数拥有一个引用参数 &T,则会为其分配一个唯一的生命周期参数,如
fn foo<'a>(x: &'a T)
。 - 如果函数只有一个输入生命周期参数,那么它将被分配给所有输出生命周期参数。例如,
fn foo<'a>(x: &'a T) -> &'a T
。 - 如果函数有多个输入生命周期参数,但其中一个是
&self
或&mut self
,则self
的生命周期将被分配给所有输出生命周期参数。例如,fn foo<'a, 'b>(&'a self, x: &'b T) -> &'a T
。
3.4.2 应用生命周期省略规则
以下是一个使用生命周期省略规则的示例,以及详细的中文注释:
// 定义一个名为 Person 的结构体,包含一个 name 字段 struct Person { name: String, } impl Person { // 使用生命周期省略规则,省略了生命周期参数 // 这里的 &self 是一个输入生命周期参数,根据规则 3,它将被分配给输出生命周期参数 fn get_name(&self) -> &str { // 返回 name 字段的引用 &self.name } } fn main() { // 创建一个 Person 结构体实例 let person = Person { name: String::from("Jack"), }; // 调用 get_name 方法获取 name 字段的引用 let name = person.get_name(); // 打印 name println!("名字: {}", name); }
输出:
名字: Jack
在这个示例中,我们定义了一个名为 Person 的结构体,它包含一个 name
字段。然后,我们为 Person 结构体实现了一个名为 get_name
的方法,该方法返回 name
字段的引用。请注意,我们没有在函数签名中显式指定生命周期参数。这是因为编译器可以根据生命周期省略规则自动推断它们。
实际上,get_name 方法的签名等同于:
fn get_name<'a>(&'a self) -> &'a str
通过应用生命周期省略规则,我们可以简化代码并提高可读性。
4. 应用所有权和生命周期
4.1 案例研究
4.1.1 案例1:高效管理内存
在这个示例中,我们将展示如何通过所有权系统高效地管理内存。
fn main() { let s = String::from("hello"); let s2 = s; // 移动所有权 // println!("{}", s); // 错误!s 的所有权已经移动到 s2 println!("{}", s2); // 正确!s2 现在拥有所有权 }
在本例中,我们创建了一个名为 s
的 String 类型变量。然后,我们将 s
的所有权移动到了名为 s2
的另一个变量。请注意,我们不能再访问 s
,因为它的所有权已经移动到了 s2
。这样可以确保内存资源的高效管理,避免了不必要的内存复制。
4.1.2 案例2:防止数据竞争
在这个示例中,我们将展示如何通过生命周期和借用规则防止数据竞争。
use std::thread; fn main() { let mut data = vec![1, 2, 3]; // 创建一个线程,对 data 进行只读访问 let t = thread::spawn(move || { // 由于 data 是只读引用,因此可以安全地在多个线程之间共享 let data = &data; println!("Data: {:?}", data); }); // 修改 data 的值 data.push(4); // 等待线程结束 t.join().unwrap(); }
在这个示例中,主要问题是试图在一个线程中读取 data,同时在另一个线程中修改 data。这可能导致数据竞争,因为两个线程试图同时访问同一资源。然而,由于 Rust 的所有权和借用规则,这个示例将无法编译:
error[E0382]: borrow of moved value: `data` --> src\main.rs:14:5 | 4 | let mut data = vec![1, 2, 3]; | -------- move occurs because `data` has type `Vec<i32>`, which does not implement the `Copy` trait ... 7 | let t = thread::spawn(move || { | ------- value moved into closure here 8 | // 由于 data 是只读引用,因此可以安全地在多个线程之间共享 9 | let data = &data; | ---- variable moved due to use in closure ... 14 | data.push(4); | ^^^^^^^^^^^^ value borrowed here after move
现在我们做出修改:
use std::sync::Arc; use std::thread; fn main() { let data = vec![1, 2, 3]; let data = Arc::new(data); // 将 data 包装在原子引用计数(Arc)中 // 创建一个线程,对 data 进行只读访问 let t = { let data = Arc::clone(&data); // 克隆 Arc,以便在新线程中安全地共享数据 thread::spawn(move || { println!("Data: {:?}", data); }) }; // 修改 data 的值 // 注意:这里不能直接修改 data,因为它已经被移动到 Arc 中 // 如果需要修改数据,可以考虑使用互斥锁(Mutex)或读写锁(RwLock) // 等待线程结束 t.join().unwrap(); }
输出为:
Data: [1, 2, 3]
这里,我们使用 std::sync::Arc
(原子引用计数)来在多个线程之间安全地共享数据。这样,当一个线程读取数据时,另一个线程无法修改数据。请注意,这个示例中的 data
是只读的,如果需要在多个线程之间共享可变数据,可以考虑使用 std::sync::Mutex
(互斥锁)或 std::sync::RwLock
(读写锁)。
通过生命周期和借用规则,Rust 能够在编译时检测潜在的数据竞争问题,从而确保程序的安全性和稳定性。
4.2 更多实践
4.2.1 练习1:实施所有权规则
在这个练习中,我们将实施所有权规则,以确保内存的有效管理。
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); // 使用 clone 方法复制 s1 的值,而不是移动所有权 println!("s1: {}", s1); // s1 的所有权未被移动,可以继续使用 println!("s2: {}", s2); // s2 拥有 s1 复制的值 }
在这个练习中,我们创建了一个名为 s1
的 String 类型变量。然后,我们使用 clone
方法复制了 s1
的值,而不是移动它的所有权。这样,我们可以继续访问 s1
,同时创建了一个名为 s2
的新变量,它拥有 s1
复制的值。
4.2.2 练习2:引用和借用的操作
在这个练习中,我们将使用引用和借用来实现一个计算字符串长度的函数。
fn main() { let s = String::from("hello"); let len = calculate_length(&s); // 使用引用,不会移动 s 的所有权 println!("The length of '{}' is {}.", s, len); } fn calculate_length(s: &String) -> usize { s.len() }
在这个练习中,我们创建了一个名为 s
的 String 类型变量,并使用引用将其传递给名为 calculate_length
的函数。这样,我们不会移动 s
的所有权,从而可以在函数调用后继续使用它。
4.2.3 练习3:应用生命周期注解
在这个练习中,我们将应用生命周期注解来实现一个返回两个字符串切片中较长者的函数。
// 使用生命周期注解 'a,确保返回的引用在输入引用的生命周期范围内 fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str { if s1.len() > s2.len() { s1 } else { s2 } } fn main() { let s1 = String::from("hello"); let s2 = String::from("world"); let result = longest(&s1, &s2); println!("最长的字符串是:'{}'。", result); }
输出为:
最长的字符串是:'world'。
在这个练习中,我们创建了两个名为 s1
和 s2
的 String 类型变量,并将它们的引用传递给名为 longest
的函数。这个函数使用生命周期注解 'a
来确保返回的引用在输入引用的生命周期范围内。这样,我们可以确保返回的引用在程序中是有效的。
4.3 总结
在本章中,我们通过案例研究和实践练习回顾了所有权、引用和生命周期的概念。所有权规则确保内存的有效管理,避免不必要的内存复制。引用和借用允许我们在不转移所有权的情况下访问变量。生命周期注解确保引用在程序中始终有效,从而避免悬垂引用和数据竞争等问题。
所有权和生命周期是 Rust 编程的核心概念,它们在确保内存安全、防止数据竞争和提高性能方面发挥着关键作用。在本章中,我们通过实际应用和练习加深了对这些概念的理解。通过深入了解所有权和生命周期的实际应用,我们可以更好地利用 Rust 的强大功能,编写高效且安全的代码。
- 悬垂引用(Dangling Reference) 是指一个指针或引用指向的对象已经被释放或失效,但该指针或引用仍然被使用的情况。这可能导致程序行为不稳定或产生未定义行为。在 Rust 中,编译器确保引用永远不会变成悬垂状态。当你拥有一些数据的引用时,编译器确保数据不会在其引用之前离开作用域。 ↩︎