文章目录

Rust 所有权:move、copy、borrow 的规则

发布于 2026-04-03 11:31:31 · 浏览 7 次 · 评论 0 条

Rust 所有权:move、copy、borrow 的规则

Rust 的所有权系统是其内存安全的核心机制,它在编译期通过一套严格的规则,确保程序不会出现悬垂指针、数据竞争等问题,而无需依赖垃圾回收。理解 movecopyborrow 是掌握 Rust 的关键。


1. 理解所有权的基本原则

Rust 中的每个值都有一个“所有者”,并且同一时间只能有一个所有者。当所有者离开作用域时,该值会被自动释放。

记住三条核心规则

  1. 每个值在任意时刻有且仅有一个所有者
  2. 当所有者离开作用域,值被丢弃(drop)
  3. 赋值或传参默认会转移所有权(move),而不是复制数据。

2. move:所有权的转移

当你将一个变量赋值给另一个变量,或者作为参数传递给函数,默认行为是 移动(move),即原变量不再有效。

执行以下操作

let s1 = String::from("hello");
let s2 = s1; // s1 被 move 到 s2
// println!("{}", s1); // ❌ 编译错误:s1 已失效
  • String 是堆分配类型,包含指针、长度和容量。
  • 如果允许 s1s2 同时拥有同一块堆内存,离开作用域时会重复释放,导致“双重释放”错误。
  • Rust 通过 move 避免此问题:赋值后,s1 不再可用。

调用函数也会触发 move

fn take_ownership(s: String) {
    println!("{}", s);
} // s 在这里被 drop

let s = String::from("world");
take_ownership(s);
// println!("{}", s); // ❌ 错误:s 已被 move

3. copy:栈上数据的隐式复制

某些类型的数据只存在于栈上,复制成本极低,Rust 允许它们在赋值或传参时自动 复制(copy),而不是 move。

哪些类型实现了 Copy trait?

  • 所有整数类型(如 i32u64
  • 布尔类型 bool
  • 浮点类型 f32f64
  • 字符 char
  • 元组(仅当所有元素都是 Copy 类型时),如 (i32, i32)

验证 copy 行为

let x = 5;
let y = x; // x 是 Copy 类型,所以这里是复制
println!("x = {}, y = {}", x, y); // ✅ 正常运行
  • xy 是独立的栈上值,互不影响。
  • Rust 不允许你为已实现 Drop trait 的类型同时实现 Copy,因为需要显式管理资源的类型不应被隐式复制。

4. borrow:临时借用而不获取所有权

有时你只想读取或修改数据,但不想拿走所有权。这时使用 借用(borrowing)

4.1 不可变借用(&T)

创建一个不可变引用

fn print_str(s: &String) {
    println!("{}", s);
}

let s = String::from("hello");
print_str(&s); // 传入 &s,s 的所有权未被转移
println!("{}", s); // ✅ 依然可用
  • &s 是对 s 的“借用”,函数参数 s: &String 表示“我只读,不拿走”。
  • 默认情况下,引用是不可变的。

4.2 可变借用(&mut T)

要修改借来的数据,必须使用可变引用

fn push_str(s: &mut String) {
    s.push_str(", world");
}

let mut s = String::from("hello");
push_str(&mut s);
println!("{}", s); // 输出 "hello, world"
  • 原变量 s 必须声明为 mut
  • 调用时传入 &mut s

5. 借用的核心限制规则

Rust 对引用施加两条硬性约束,防止数据竞争:

  1. 在任意时刻,要么有多个不可变引用(&T),要么有一个可变引用(&mut T),不能同时存在
  2. 引用必须始终有效(不能悬垂)

5.1 引用互斥规则示例

let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
// println!("{}, {}", r1, r2); // ✅ 允许多个不可变引用

let r3 = &mut s; // ❌ 编译错误!
// 不能在已有不可变引用时创建可变引用

正确做法:让不可变引用的作用域提前结束:

let mut s = String::from("hello");

{
    let r1 = &s;
    let r2 = &s;
    println!("{}, {}", r1, r2);
} // r1, r2 在这里失效

let r3 = &mut s; // ✅ 现在可以创建可变引用

5.2 悬垂引用禁止

返回局部变量的引用会导致悬垂指针,Rust 禁止此行为

fn dangle() -> &String {
    let s = String::from("hello");
    &s // ❌ 错误:s 在函数结束时被 drop,引用无效
}

编译器会报错:“s does not live long enough”。

解决方法:直接返回值(而非引用):

fn no_dangle() -> String {
    let s = String::from("hello");
    s // move 出去,所有权转移给调用者
}

6. 总结三种行为的适用场景

下表列出何时发生 move、copy 或 borrow:

操作 数据类型 发生的行为
赋值 let y = x; 实现 Copy(如 i32 copy
赋值 let y = x; 未实现 Copy(如 String move
函数传参 f(x) Copy 类型 copy
函数传参 f(x) Copy 类型 move
函数传参 f(&x) 任意类型 borrow(不可变)
函数传参 f(&mut x) mut x borrow(可变)

7. 实践建议:如何写出符合所有权规则的代码

  1. 优先使用引用传递:除非你需要转移所有权,否则函数参数尽量用 &T&mut T
  2. 字符串字面量用 &str:函数参数若只读字符串,用 &str 而非 &String,兼容性更好。
    fn greet(name: &str) { // 接受 &str
        println!("Hello, {}", name);
    }
    greet("Alice");        // 字符串字面量是 &str
    let s = String::from("Bob");
    greet(&s);             // &String 可自动转为 &str
  3. 避免不必要的 clone:不要为了绕过 move 而随意调用 .clone(),这会带来运行时开销。先考虑是否能用引用。
  4. 利用作用域控制借用生命周期:用 {} 显式结束引用的作用域,以便后续创建可变引用。

重构 move 为 borrow 的例子

原始(低效):

fn calculate_len(s: String) -> (String, usize) {
    let length = s.len();
    (s, length) // 必须返回 s,否则调用者无法再用
}

改进(推荐):

fn calculate_len(s: &String) -> usize {
    s.len() // 不拿所有权,调用者继续使用
}

进一步优化(使用 &str):

fn calculate_len(s: &str) -> usize {
    s.len()
}

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文