所有权与借用
这篇讲 Rust 最核心的概念——所有权(Ownership)与借用(Borrowing)。这是 Rust 区别于所有主流语言的根本机制,理解它就是掌握了 Rust 的钥匙。
为什么需要所有权
主流语言管理内存的三种方式:
- C/C++:程序员手动
malloc/free、new/delete——灵活但容易出错 - Java/Go/Python:垃圾回收器(GC)自动回收——省心但有运行时开销
- Rust:所有权系统——编译期检查,无运行时开销,无 GC 停顿
所有权规则(三条铁律):
- Rust 中每个值都有且仅有一个所有者(owner)
- 当所有者离开作用域,值被自动释放
- 值在任何时刻只能有一个所有者(也有例外——后面会讲
Rc)
作用域与 Drop
fn main() {
{ // s 尚未声明
let s = String::from("hello"); // s 从此处开始有效
println!("{s}");
} // s 离开作用域,内存自动释放
// println!("{s}"); // ❌ s 已经不存在了
}这类似于 C++ 的 RAII(Resource Acquisition Is Initialization),值在离开作用域时调用 drop。
移动(Move)
先理解栈和堆:栈上的数据大小固定(如 i32),拷贝成本低;堆上的数据大小不固定(如 String),拷贝需要深复制。Rust 对这两种数据的「赋值」处理不同:
fn main() {
// 栈数据:自动 Copy
let x = 5;
let y = x; // x 被复制,x 仍然可用
println!("x={}, y={}", x, y); // ✅ 都可用
// 堆数据:Move(所有权转移)
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移给了 s2
// println!("{s1}"); // ❌ s1 已经失效!
println!("{s2}"); // ✅ s2 是新的所有者
}为什么要 move 而不是 copy?——String 在堆上的数据如果被「浅拷贝」,两个指针指向同一块内存,离开作用域时会 double free。Rust 直接从编译期禁止了这种可能性。
函数传参和返回值也会触发 move:
fn main() {
let s = String::from("hello");
takes_ownership(s); // s 的所有权移入函数
// println!("{s}"); // ❌ s 已经不可用
let s2 = gives_ownership(); // 函数返回值的所有权移给 s2
println!("{s2}");
}
fn takes_ownership(s: String) {
println!("{s}");
} // s 被 drop
fn gives_ownership() -> String {
let s = String::from("world");
s // 所有权移出函数
}克隆(Clone)
如果真的需要深拷贝堆数据,显式调用 .clone():
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝,堆数据被复制
println!("s1={}, s2={}", s1, s2); // ✅ 两者都可用
}Copy trait
为什么 i32 赋值后还能用?因为它实现了 Copy trait。实现了 Copy 的类型在赋值时自动按位复制,不会触发 move。
实现了 Copy 的类型:
- 所有整数、浮点数、布尔、字符类型
- 元素都是 Copy 的元组,如
(i32, i32) - 不可变引用
&T
String、Vec 等涉及堆分配的类型没有实现 Copy——因为这相当于隐式深拷贝,违背了 Rust「显式表达成本」的理念。
引用与借用
每次都 move 所有权太麻烦了——函数用完参数还得把所有权还回来。Rust 提供了**引用(Reference)**机制,允许「借用」值而不获取所有权:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // &s1 创建一个指向 s1 的引用
println!("'{s1}' 的长度是 {len}"); // ✅ s1 仍然可用
}
fn calculate_length(s: &String) -> usize { // &String = 引用类型
s.len()
} // s 离开作用域,但它不拥有值,所以不 drop
& 创建引用,这种行为叫借用(Borrowing)——你借了别人的东西,用完得还。
可变引用
默认引用是不可变的。要修改借用的值,需要 &mut:
fn main() {
let mut s = String::from("hello");
change(&mut s); // 可变引用
println!("{s}"); // "hello, world!"
}
fn change(s: &mut String) {
s.push_str(", world!");
}可变引用的铁律:在同一作用域中,对于同一块数据:
- 要么有任意多个不可变引用
- 要么有恰好一个可变引用
- 两者不能同时存在
fn main() {
let mut s = String::from("hello");
let r1 = &s; // ✅ 不可变引用
let r2 = &s; // ✅ 可以有多个不可变引用
// let r3 = &mut s; // ❌ 不能同时有不可变和可变引用!
println!("{r1} {r2}"); // r1、r2 最后一次使用在此
let r3 = &mut s; // ✅ r1、r2 不再使用,可以创建可变引用
r3.push_str("!");
}悬垂引用
Rust 编译器保证永远不会出现悬垂引用——指向已释放内存的引用:
// ❌ 编译错误
fn dangle() -> &String {
let s = String::from("hello");
&s // s 在函数结束时被释放,返回的引用指向无效内存!
} // 编译器会报错:missing lifetime specifier / returns a reference to data owned by the current function
切片(Slice)
切片是集合中一段连续元素的引用,不拥有数据:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
let all = &s[..]; // "hello world"
// 更常用的场景:函数参数用 &str 代替 &String
let word = first_word(&s);
println!("第一个单词: {}", word); // "hello"
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}&str(字符串切片)是 Rust 中处理字符串的首选类型——比 &String 更通用,类似 Go 中推荐用 io.Reader 而非具体类型。绝大多数接受字符串参数的函数应该用 &str。一句话小结
所有权是 Rust 的基石:每个值有唯一所有者,离开作用域自动释放。赋值/传参默认是 move,要保留原值用 .clone() 或借用(&/&mut)。可变引用最多一个,且不能与不可变引用共存——这是 Rust 编译期消灭数据竞争的秘诀。下一篇讲所有权的延伸——生命周期。
练习
- 以下代码能否编译?为什么?
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // 这里?
println!("{r1}");
}- 写一个函数
longest<'a>(x: &'a str, y: &'a str) -> &'a str,返回两个字符串切片中较长的那个。(提示:需要生命周期标注,下一篇会详细讲。)
参考答案
- 不能编译。
r1是不可变引用,r2是可变引用,Rust 不允许同时存在。把println!("{r1}")移到let r2之前就可以编译——这是「非词法作用域生命周期」(NLL)的特性。 - 暂时跳过——生命周期标注将在下一篇详解。