文章
Rust:函数传参一定会 move 吗?从 Tuple、Copy 到 Ownership
通过 tuple 传参示例,理解 Rust 中按值传参、move、Copy,以及为什么同一个变量可以被多次作为函数参数传递。
目录
很多 Rust 初学者第一次看到下面的代码时,会产生一个非常自然的疑问:coords 第一次作为参数传给 move_coords 后,不是应该被 move 掉了吗?为什么第二次还能继续使用?
1. 示例:为什么 coords 可以使用两次?#
fn main() {
let coords = (0, 1);
let result = move_coords(coords);
assert_eq!(result, (1, 2));
let (x, y) = move_coords(coords);
assert_eq!(x, 1);
assert_eq!(y, 2);
}
fn move_coords(x: (i32, i32)) -> (i32, i32) {
(x.0 + 1, x.1 + 1)
}
这段代码可以正常编译。关键不在于函数名 move_coords,而在于参数类型 (i32, i32) 是否实现了 Copy。
2. 按值传参并不意味着一定发生 move#
看到 foo(x) 时,不要机械地认为 x 一定被 move。
更准确的判断方式是:
- 按值传递的是
Copy类型:复制值,原变量仍然有效。 - 按值传递的是非
Copy类型:所有权转移,原变量通常不能继续使用。 因此,“函数传参 = move”是一个常见但不准确的初学者模型。
3. 为什么 (i32, i32) 是 Copy?#
i32 实现了 Copy。Rust 对 tuple 有一个重要规则:当 tuple 的所有元素都实现 Copy 时,这个 tuple 也实现 Copy。
i32 → Copy
i32 → Copy
────────────────────
(i32,i32) → Copy
所以第一次调用 move_coords(coords) 时,函数参数获得的是 coords 的 Copy,而原来的 coords 仍然有效。第二次调用自然也可以继续使用它。
4. 从所有权角度看发生了什么?#
可以简化理解成:
coords = (0, 1)
│
├── Copy ──→ 第一次 move_coords 的参数 x
│
└── Copy ──→ 第二次 move_coords 的参数 x
函数名 move_coords 不会决定所有权行为。它只是普通的函数名。真正决定行为的是参数的类型。
5. 换成 String,情况完全不同#
fn foo(s: String) {
println!("{s}");
}
fn main() {
let s = String::from("hello");
foo(s);
println!("{s}");
}
这段代码无法编译,因为 String 没有实现 Copy。
第一次调用 foo(s) 会把 s 的所有权转移给函数参数。函数结束后,参数负责释放这个 String,调用方原来的变量已经不能继续使用。
6. 为什么 String 不能 Copy?#
String 管理着堆上的字符串数据,可以抽象成:
栈上:
┌──────────────┐
│ ptr │ ───────→ 堆上的 "hello"
│ len │
│ capacity │
└──────────────┘
如果 String 可以隐式 Copy,两个 String 可能拥有指向同一块堆内存的状态,并最终重复释放同一资源。因此 Rust 不允许这种类型实现 Copy,而采用 move 语义来保证资源只有一个拥有者。
严格来说,move 在底层通常可能只是复制栈上的表示;关键区别在于 Rust 会让旧变量失效,从而避免双重释放等问题。
7. Tuple 的 Copy 是递归的#
let a = (1, 2, 'c');
这个 tuple 是 Copy,因为 i32、i32 和 char 都是 Copy。
但:
let b = (String::from("hello"), 1);
不是 Copy,因为其中的 String 不是 Copy。
可以记成:
i32 → Copy
i32 → Copy
char → Copy
──────────────
Tuple → Copy
而:
String → 非 Copy
i32 → Copy
───────────────
Tuple → 非 Copy
8. 一个实用的判断模型#
以后看到:
foo(x);
按照下面的顺序判断:
x 按值传参
│
▼
查看 x 的类型
│
┌─────┴─────┐
│ │
Copy 类型 非 Copy 类型
│ │
▼ ▼
Copy Move
│ │
▼ ▼
x 仍然可用 x 失效
常见类型:
| 类型 | Copy | 按值传参后原变量 |
|---|---|---|
i32 | ✅ | 可以继续使用 |
u64 | ✅ | 可以继续使用 |
f64 | ✅ | 可以继续使用 |
bool | ✅ | 可以继续使用 |
char | ✅ | 可以继续使用 |
(i32, i32) | ✅ | 可以继续使用 |
(i32, char) | ✅ | 可以继续使用 |
&str | ✅ | 可以继续使用 |
String | ❌ | move 后不能继续使用 |
Vec<i32> | ❌ | move 后不能继续使用 |
Box<T> | ❌ | move 后不能继续使用 |
9. Copy 与 move 的核心关系#
按值使用一个值
│
▼
类型是否 Copy?
/ \
是 否
│ │
▼ ▼
Copy Move
│ │
▼ ▼
原变量可用 原变量失效
因此,学习 Rust 所有权时不要形成“函数传参就会 move”的固定印象。 更准确的模型是:
按值传参 +
Copy类型 = Copy,原变量仍然有效。按值传参 + 非
Copy类型 = move,原变量不再拥有该值。
10. 最后再看开头的代码#
let coords = (0, 1);
let result = move_coords(coords);
assert_eq!(result, (1, 2));
let (x, y) = move_coords(coords);
assert_eq!(x, 1);
assert_eq!(y, 2);
这里的关键链路只有一条:
coords
↓
(i32, i32)
↓
(i32, i32): Copy
↓
第一次调用执行 Copy
↓
coords 仍然有效
↓
第二次调用仍然合法
不要只根据语法形式判断 Rust 的所有权行为,要结合具体类型的 Copy / 非 Copy 语义来判断。