Rust 所有权与借用详解
概述
所有权(Ownership)是 Rust 最独特的特性,它让 Rust 无需垃圾回收器就能保证内存安全。理解所有权对于掌握 Rust 至关重要。
所有权的三条规则
-
Rust 中的每一个值都有一个被称为其所有者(owner)的变量
-
值在任一时刻有且只有一个所有者
-
当所有者(变量)离开作用域,这个值将被丢弃
栈与堆
栈(Stack)
-
后进先出(LIFO)
-
存储已知大小的数据
-
快速高效
-
所有数据必须占用已知且固定的大小
堆(Heap)
-
用于存储编译时未知大小的数据
-
分配内存时需要在堆上找一块足够大的空间
-
通过指针访问堆上的数据
-
分配和释放比栈慢
fn main() {
// 栈上分配的值
let x = 5;
let y = x; // x 仍然有效,因为实现了 Copy trait
println!("x = {}, y = {}", x, y);
// 堆上分配的值
let s1 = String::from("hello");
let s2 = s1; // s1 已经失效,所有权转移给 s2
// println!("{}", s1); // 错误!s1 已经失效
println!("{}", s2);
}
所有权机制
移动(Move)
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移动到 s2
// s1 不再有效
// println!("{}", s1); // 编译错误
println!("{}", s2);
}
克隆(Clone)
如果需要深度复制,可以使用 clone 方法:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深度复制
println!("s1 = {}, s2 = {}", s1, s2);
}
Copy trait
实现了 Copy trait 的类型在赋值时会自动复制:
fn main() {
let x = 5;
let y = x; // x 仍然有效
println!("x = {}, y = {}", x, y);
// 实现 Copy 的类型:
// - 所有整数类型
// - 布尔类型
// - 浮点数类型
// - 字符类型
// - 元组(如果所有元素都实现了 Copy)
}
函数与所有权
传递值给函数
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数中
// s 不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 移动到函数中
println!("x = {}", x); // x 仍然有效
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,`drop` 被调用,内存被释放
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer 离开作用域,但不需要特殊处理
返回值与作用域
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值移动给 s1
println!("s1 = {}", s1);
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 移动到函数中,返回值移动给 s3
println!("s3 = {}", s3);
// s2 不再有效
}
fn gives_ownership() -> String {
let some_string = String::from("hello"); // some_string 进入作用域
some_string // some_string 被返回并移动给调用者
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // a_string 被返回并移动给调用者
}
引用与借用
不可变引用
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 借用 s1
println!("'{}' 的长度是 {}", s1, len);
}
fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
s.len()
} // s 离开作用域,但因为它没有所有权,所以不释放内存
可变引用
fn main() {
let mut s = String::from("hello");
change(&mut s); // 可变借用
println!("{}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
引用的规则
-
在任意给定时间,要么:
- 只能有一个可变引用
-
或者:
- 只能有多个不可变引用
-
引用必须始终有效
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} 和 {}", r1, r2);
// r1 和 r2 不再使用了
let r3 = &mut s; // 没问题
println!("{}", r3);
// 以下代码会编译错误:
// let r1 = &mut s;
// let r2 = &mut s; // 错误!不能有多个可变引用
}
悬垂引用
fn main() {
// let reference_to_nothing = dangle();
}
// 返回引用的函数不能正确工作
// fn dangle() -> &String { // 错误!返回引用的字符串
// let s = String::from("hello"); // s 是一个新 String
// &s // 返回 s 的引用
// } // s 离开作用域并被丢弃,其内存被释放
// // 返回的引用指向一个无效的 String
// 正确的做法是返回 String
fn no_dangle() -> String {
let s = String::from("hello");
s // s 的所有权被移动给调用者
}
切片(Slice)
字符串切片
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // 字符串切片
let world = &s[6..11];
println!("hello = {}, world = {}", hello, world);
// 从开头开始可以省略 0
let slice = &s[..5];
// 到结尾可以省略最后一个数字
let slice = &s[6..];
// 同时省略两个值可以获取整个字符串
let slice = &s[..];
println!("切片: {}", slice);
}
字符串字面量就是切片
fn main() {
let s = "Hello, world!"; // s 的类型是 &str
println!("{}", s);
}
在函数中使用字符串切片
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// first_word 适用于 String 的切片
let word = first_word(&my_string[0..6]);
println!("第一个词: {}", word);
let word = first_word(&my_string[..]);
println!("第一个词: {}", word);
// first_word 也适用于 String 的引用
let word = first_word(&my_string);
println!("第一个词: {}", word);
// first_word 也适用于字符串字面量
let word = first_word("hello world");
println!("第一个词: {}", word);
}
其他类型的切片
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
println!("数组切片: {:?}", slice);
assert_eq!(slice, &[2, 3]);
}
实际应用示例
查找第一个单词
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {
let mut s = String::from("hello world");
let word_index = first_word(&s); // 返回索引
s.clear(); // 清空字符串
// word_index 在这里仍然有效!但字符串已经改变
// 这可能导致问题
}
改进版本,使用字符串切片:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // 返回切片
s.clear(); // 编译错误!
// println!("第一个词是: {}", word); // 编译器会阻止这行代码
}
总结
本教程详细介绍了 Rust 的所有权与借用机制:
-
所有权规则:
- 每个值都有一个所有者
- 值在任一时刻只有一个所有者
- 所有者离开作用域时值被丢弃
-
移动与克隆:
- 默认情况下,赋值会移动所有权
- 使用
clone方法进行深度复制 - 实现了
Copytrait 的类型会自动复制
-
函数与所有权:
- 传递值给函数会移动所有权
- 返回值可以传递所有权
-
引用与借用:
- 引用允许在不获取所有权的情况下使用值
- 有不可变引用和可变引用
- 引用的规则保证内存安全
-
切片:
- 引用数组或字符串的一部分
- 字符串切片是
&str - 使用切片可以避免所有权问题
所有权是 Rust 最核心的特性,它使 Rust 能够在编译时保证内存安全,而无需垃圾回收器。虽然一开始可能需要一些时间来适应,但一旦理解了所有权,你就能编写出安全、高效的 Rust 代码。
下一步
在下一教程中,我们将学习 Rust 的结构体与枚举,这是 Rust 中定义自定义类型的重要方式。我们将了解:
-
定义和使用结构体
-
方法和关联函数
-
枚举和模式匹配
-
Option 类型
-
match 表达式的深入应用
继续学习 Rust,掌握这门强大语言的更多特性!

