Rust 入门教程 2:所有权、借用与引用
在上一教程中,我们学习了 Rust 的基础语法、变量绑定和数据类型。今天,我们将深入 Rust 最核心的概念:所有权(Ownership)、借用(Borrowing) 和 引用(References)。这些概念是 Rust 内存安全保证的基石,也是初学者最需要花时间理解的部分。
1. 所有权(Ownership)
所有权是 Rust 独特的内存管理系统。它既没有垃圾回收器(GC),也不依赖程序员手动分配和释放内存,而是通过一套规则在编译时进行检查。
1.1 所有权规则
1.2 作用域
与很多编程语言类似,Rust 使用大括号 {} 来定义作用域。变量在作用域内有效。
fn main() {
// s 在这里无效,因为它尚未声明
let s = \\"hello\\"; // s 从这里开始有效
println!(\\"{}\\", s);
} // 作用域结束,s 不再有效
1.3 移动(Move)
让我们来看一个稍复杂的例子。这里涉及到 String 类型,它存储在堆上。
fn main() {
let s1 = String::from(\\"hello\\");
let s2 = s1;
// println!(\\"{}\\", s1); // 这行会导致编译错误!
println!(\\"{}\\", s2); // 正常运行
}
为什么 s1 不能再使用了?这是因为 let s2 = s1 发生了一次 移动(Move)。
- String 由三部分组成:指向堆内存的指针、长度、容量。这些数据存储在栈上。
- 真正的字符串内容 \\"hello\\" 存储在堆上。
- 当我们将 s1 赋值给 s2 时,Rust 浅拷贝了栈上的指针、长度和容量。
- 为了避免 s1 和 s2 同时指向同一块堆内存(导致“双倍释放”错误),Rust 立即将 s1 置为无效。
- 因此,s1 的所有权被 移动 到了 s2。
1.4 克隆(Clone)
如果我们确实需要深度复制堆上的数据,可以使用 clone 方法。
fn main() {
let s1 = String::from(\\"hello\\");
let s2 = s1.clone();
println!(\\"s1 = {}, s2 = {}\\", s1, s2); // 现在都可以使用了
}
1.5 栈数据:复制(Copy)
像整数、布尔、浮点数这些在编译时大小已知的类型,完全存储在栈上,因此复制它们非常快。对于这类实现了 Copy trait 的类型,赋值操作会进行完整的复制,不会发生移动。
fn main() {
let x = 5;
let y = x; // x 没有被移动,而是被复制了
println!(\\"x = {}, y = {}\\", x, y); // x 仍然有效
}
1.6 函数与所有权
将变量传递给函数也会发生移动或复制。
fn main() {
let s = String::from(\\"hello\\");
takes_ownership(s); // s 的值移动到函数里
// println!(\\"{}\\", s); // 这里 s 已经无效,编译错误
let x = 5;
makes_copy(x); // x 是 Copy 类型,仍然有效
println!(\\"{}\\", x); // 正常运行
}
fn takes_ownership(some_string: String) {
println!(\\"{}\\", some_string);
} // some_string 离开作用域,内存被释放
fn makes_copy(some_integer: i32) {
println!(\\"{}\\", some_integer);
}
1.7 返回值与所有权
函数返回值也可以转移所有权。
fn main() {
let s1 = gives_ownership(); // 返回值移动到 s1
let s2 = String::from(\\"hello\\");
let s3 = takes_and_gives_back(s2); // s2 移动到函数,函数再将返回值移动到 s3
// println!(\\"{}\\", s2); // s2 已经无效
println!(\\"s1 = {}, s3 = {}\\", s1, s3);
}
fn gives_ownership() -> String {
let some_string = String::from(\\"hello\\");
some_string // 返回,所有权转移给调用者
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 返回,所有权转移给调用者
}
2. 引用与借用
每次都要把所有权传来传去很麻烦。如果我们只是想使用一个值而不获取其所有权,可以使用 引用(References)。
2.1 引用
引用允许你引用某个值而不获取其所有权。
fn main() {
let s1 = String::from(\\"hello\\");
let len = calculate_length(&s1); // 传递 s1 的引用
println!(\\"The length of '{}' is {}.\\", s1, len); // s1 仍然有效
}
fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
s.len()
} // s 离开作用域,但它不拥有所有权,所以不会发生 drop
这里 &s1 语法创建了一个指向 s1 值的引用,但没有获取它的所有权。因为不拥有它,所以当引用离开作用域时,它指向的值不会被丢弃。这种通过引用传递参数的行为称为 借用(Borrowing)。
2.2 可变引用
默认情况下,引用是不可变的。我们可以使用 &mut 来创建可变引用,从而修改借用的值。
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\\");
}
可变引用有一个重要限制:在特定作用域内,对于某一块数据,只能有一个活跃的可变引用。
let mut s = String::from(\\"hello\\");
let r1 = &mut s;
let r2 = &mut s; // 编译错误:不能多次借用 s 作为可变引用
这个限制防止了数据竞争。
2.3 引用的规则
总结一下引用的规则:
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的(不能是悬垂引用)。
3. 切片(Slice)
切片是另一种不持有所有权的数据类型。它允许你引用集合中的一段连续元素序列,而不是整个集合。
3.1 字符串切片
fn main() {
let s = String::from(\\"hello world\\");
let hello = &s[0..5]; // 包含 0,不包含 5
let world = &s[6..11]; // 包含 6,不包含 11
println!(\\"{}, {}\\", hello, world); // 输出 \\"hello, world\\"
}
.. 是范围语法,有多种写法:
let s = String::from(\\"hello\\");
let slice1 = &s[0..2]; // \\"he\\"
let slice2 = &s[..2]; // 从开头到索引 2,等同于 &s[0..2]
let slice3 = &s[2..5]; // \\"llo\\"
let slice4 = &s[2..]; // 从索引 2 到结尾
let slice5 = &s[..]; // 整个字符串
3.2 字符串字面量就是切片
还记得我们之前说的 let s = \\"hello\\"; 吗?这里的 s 实际上是一个 &str 类型,它是一个指向二进制程序特定位置的切片。
let s = \\"hello world\\"; // s: &str
3.3 其他类型的切片
数组也可以有切片:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 类型是 &[i32],包含 [2, 3]
4. 总结
本章我们学习了 Rust 的核心概念:
- 所有权:每个值都有唯一的所有者,所有者离开作用域时值被释放
- 移动:赋值或传参时,所有权可能被转移
- 借用:通过引用访问值而不获取所有权
- 引用规则:要么一个可变引用,要么多个不可变引用
- 切片:对集合的部分引用
这些概念是理解 Rust 内存安全的基础。虽然一开始可能有些抽象,但随着练习会逐渐熟悉。下一章我们将学习 Rust 的结构体和方法!
网硕互联帮助中心




评论前必须登录!
注册