4. Rust 所有权
4.1 所有权
所有权特性让 Rust 无需垃圾回收,同时保证内存安全。所有权特性不会减慢程序的运行速度,因为所有权特性是在编译时检查的。
Rust 对数据存放在堆上和栈上有着严格的规定。栈上的数据大小固定,数据存放在堆上的大小是动态的。
所有权解决的问题:
- 跟踪代码的哪些部分正在使用堆上的哪些数据
- 最小化堆上的重复数据
- 清理堆上不再使用的数据
所有权规则:
- Rust 中的每一个值都有一个被称为其所有者的变量
- 一次只能有一个所有者
- 当所有者(变量)离开作用域,这个值将被丢弃
String
类型的数据存放在堆上,字符串也是可以被修改的:
fn main() {
let mut s = String::from("hello");
s.push_str(", world!");
println!("{}", s);
}
当变量生命周期结束时,Rust 会自动调用 drop()
函数来释放内存。
String
数据的结构(栈):
名称 | 内容 |
---|---|
ptr | 指向堆上数据的指针 |
len | 长度 |
capacity | 容量 |
Rust 使用移动(move)来处理堆上数据的所有权。当变量赋值给另一个变量时,所有权会从一个变量转移到另一个变量。
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // error: value borrowed here after move
}
在这个例子中,Rust 阻止我们在将 s1
移动给 s2
后继续使用 s1
,这样可以避免"悬垂指针"问题。
Rust 为了内存安全:
- 没有尝试复制被分配的内存
- 当
s1
被移动到s2
后,s1
无效了 s1
离开作用域后,也不需要释放任何内存
使用 .clone()
方法对 String
进行深拷贝:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2); // 有效,s1 仍然可用
}
拥有 Copy trait 的类型没有所有权问题,而所有需要使用堆内存的类型都没有 Copy trait。
以下类型都实现了 Copy trait:
- 所有整数类型,如
u32
- 布尔类型
bool
- 浮点类型,如
f64
- 字符类型
char
- 仅包含实现了 Copy 的类型的元组,如
(i32, i32)
当元组的所有元素都有 Copy trait 时,此元组也有 Copy trait。
函数的参数和返回值也涉及所有权:
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里
// 所以到这里 s 不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动到函数里
// 但 i32 是 Copy 的,所以在后面可以继续使用 x
println!("x: {}", x); // 这行正常工作
// println!("s: {}", s); // 这行会报错
} // 这里,x 离开作用域,然后是 s。但因为 s 的值已被移动,没有特殊操作发生。
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
println!("s1: {}, s3: {}", s1, s3);
// println!("s2: {}", s2); // 这行会报错,s2 已被移动
} // 这里,s3, s1 离开作用域并被丢弃,s2 已被移动,所以什么也不会发生
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,所有权转移给调用者
}
4.2 引用和借用
每次传递所有权并返回所有权比较繁琐。Rust 提供了"引用"机制,允许我们使用值但不获取所有权。
fn main() {
let s1 = String::from("hello");
// &s1 创建一个指向 s1 的引用,但没有获取所有权
let len = calculate_length(&s1);
println!("'{}'的长度是 {}.", s1, len); // s1 依然有效,因为只是借用了引用
}
fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
s.len()
} // 这里,s 离开作用域,但它只是一个引用,不拥有所引用的值,所以没有任何特殊操作
将"创建一个引用"的行为称为"借用"(borrowing)。默认情况下,引用是不可变的。
可变引用
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // 输出 "hello, world"
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
可变引用有一个重要限制:在特定作用域中,对于某一块数据,只能有一个可变引用:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 错误:不能同时有两个可变引用
println!("{}, {}", r1, r2);
}
这个限制的好处是可以防止数据竞争。数据竞争在以下三种行为同时发生时会出现:
- 两个或更多指针同时访问同一数据
- 至少有一个指针被用来写入数据
- 没有同步数据访问的机制
不能在同一时间同时拥有一个可变引用和一个不可变引用:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 错误:不能在拥有不可变引用的同时拥有可变引用
println!("{}, {}, and {}", r1, r2, r3);
}
引用的作用域从声明的地方开始一直持续到最后一次使用为止:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 和 r2 作用域在这里结束,因为它们不再被使用
let r3 = &mut s;
println!("{}", r3);
} // r3 作用域结束
悬垂引用
Rust 编译器确保引用永远不会变成悬垂引用:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃,其内存被释放
// 危险!这会返回一个指向被释放内存的引用
上面的函数会编译错误,Rust 不允许悬垂引用存在。正确做法是直接返回 String:
fn no_dangle() -> String {
let s = String::from("hello");
s // 所有权被移动出去,所以没有值被释放
}
4.3 切片(Slice)类型
切片让你可以引用集合中部分连续的元素序列,而不用引用整个集合。切片是一种不持有所有权的引用。
字符串切片是 String 中一部分值的引用:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{} {}", hello, world); // 输出: hello world
}
创建切片的语法:
let s = String::from("hello");
let slice1 = &s[0..2]; // 包括索引0但不包括索引2
let slice2 = &s[..2]; // 如果从0开始,可以省略
let slice3 = &s[3..]; // 如果到末尾,可以省略
let slice4 = &s[..]; // 整个字符串的切片
例子:编写一个函数,接收一个字符串,返回其中第一个单词:
fn first_word(s: &String) -> &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(); // 错误:不能清空 s,因为 word 是一个引用
println!("第一个单词是: {}", word);
}
字符串字面值就是切片:
let s = "Hello, world!"; // s 的类型是 &str
可以将函数参数改为接受切片类型,这样更加通用:
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");
// 可以传入 String 的切片
let word = first_word(&my_string[..]);
// 也可以传入字符串字面值的切片
let word2 = first_word("hello world");
println!("{} {}", word, word2);
}
其他切片类型:
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 类型是 &[i32]
assert_eq!(slice, &[2, 3]);
}
4.4 所有权实践
Rust 的所有权系统通过编译时规则确保内存安全,而不需要垃圾回收。理解所有权、借用和生命周期是掌握 Rust 的关键。
所有权系统的优点:
- 内存安全无需垃圾回收
- 编译时检查避免运行时错误
- 并发安全性更高
总结:
- 每个值只有一个所有者
- 当所有者离开作用域,值将被丢弃
- 可以通过引用借用值而不获得所有权
- 引用可以是可变的或不可变的,但有限制
- 切片是集合一部分的引用
掌握这些概念是编写高效、安全的 Rust 程序的基础。