所有权是 Rust 最独特的特性,它使 Rust 无需垃圾回收器就能保证内存安全。所有权系统由一组规则组成,编译器会在编译时检查这些规则。所有权的核心规则包括:
所有权有两个重要的 Trait, 只能选择实现其中一个:
所有权是 Rust 管理内存的一套规则,它在编译时由编译器检查,不会对程序运行时产生性能开销。
作用域是程序中项目有效的范围。例如:
{ // s 在此处无效
let s = "hello"; // s 从此处开始有效
// 使用 s
} // 此作用域结束,s 不再有效
Rust 中有两个主要的内存区域:栈(Stack)和堆(Heap):
当我们将一个包含堆数据的变量赋值给另一个变量时,Rust 会执行”移动”操作而非拷贝:
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权被移动到 s2,s1 不再有效
在这种情况下,s1 被认为不再有效,这样可以避免”二次释放”(double free)错误。
如果我们确实需要深度复制堆上的数据而不仅是栈上的引用,可以使用 clone
方法:
let s1 = String::from("hello");
let s2 = s1.clone(); // 堆数据被复制
对于完全存储在栈上的简单标量值,赋值操作执行的是复制而非移动:
let x = 5;
let y = x; // x 仍然有效,因为整数类型实现了 Copy trait
实现了 Copy
trait 的类型在赋值给另一个变量后仍然有效。包括:
i32
、u32
等)bool
)f64
等)char
)Copy
的类型的元组,如 (i32, i32)
函数参数的传递和返回也遵循所有权规则:
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值被移动到函数中
// 之后 s 不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该被移动,但 i32 是 Copy 的
// 所以 x 在之后仍可以使用
}
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 离开作用域,无特殊操作
返回值也可以转移所有权:
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值移动给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到函数中,
// 返回值移动到 s3
}
fn gives_ownership() -> String {
let some_string = String::from("yours"); // some_string 进入作用域
some_string // 返回 some_string 并移出给调用的函数
}
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
为了解决反复传递所有权的繁琐问题,Rust 提供了”引用”机制,允许使用值但不获取其所有权。
通过 &
符号创建引用,允许你引用值而不获取其所有权:
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()
} // 这里 s 离开作用域,但因为它没有所有权,所以不会发生任何特殊情况
这种”创建引用”的行为被称为”借用”(borrowing)。
允许修改借用的值,使用 &mut
创建:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
限制:在同一时间内,对同一数据只能有一个可变引用。这个限制可以防止数据竞争(data race)。
数据竞争出现在以下三个行为同时发生时:
此外,不能同时拥有不可变引用和可变引用,因为可变引用可能会使不可变引用失效。
Rust 编译器确保引用永远不会变成悬垂引用(指向已被释放的内存位置的指针):
fn dangle() -> &String { // 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃,其内存被释放
// 危险!
这段代码会导致编译错误,因为 s
在函数结束时被释放,返回的引用将指向无效内存。
切片允许你引用集合中的一部分连续元素序列,而不是整个集合。切片是一种引用,没有所有权。
字符串切片是对 String 部分内容的引用,看起来像这样:
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
字符串切片的类型标记为 &str
。
字符串字面量的类型实际上是 &str
:
let s = "Hello, world!"; // s 的类型是 &str
这是一个指向二进制程序特定位置的切片,这也是字符串字面量不可变的原因。
将函数参数定义为接收字符串切片可以使 API 更加通用:
fn first_word(s: &str) -> &str {
// 实现
}
// 可以同时接受 String 引用或切片
let my_string = String::from("hello world");
let word1 = first_word(&my_string[..]); // 用于整个 String 的切片
let word2 = first_word(&my_string); // 用于 String 的引用
// 也可以接受字符串字面量或其切片
let my_string_literal = "hello world";
let word3 = first_word(&my_string_literal[..]);
let word4 = first_word(my_string_literal); // 字符串字面量已经是切片了
切片不仅限于字符串。例如,数组也可以有切片:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 类型是 &[i32]
Rust 的所有权、借用和切片概念确保了程序在编译时的内存安全性,无需垃圾回收器或手动内存管理。所有权系统的关键优势在于:
所有权是 Rust 的基础,它影响了 Rust 中许多其他功能的工作方式,是理解和掌握 Rust 编程的关键概念。通过深入理解所有权,我们能够编写既安全又高效的 Rust 代码。
好好学习,天天向上