C 程序员看赋值和传参,第一反应通常是“复制一份值”:
uint32_t a = 10;
uint32_t b = a;
结构体也可以按值复制:
struct Point p1 = {1, 2};
struct Point p2 = p1;
但一旦结构体里有指针,事情就变复杂:
struct Buffer {
uint8_t *data;
size_t len;
};
struct Buffer b2 = b1; // 只是复制指针和长度
这不是深拷贝。b1.data 和 b2.data 指向同一块内存。谁释放?能不能同时改?释放一个以后另一个还能不能用?C 里这些问题靠约定处理。
Rust 把这类差异拆成三件事:
move -> 所有权转移,原值不能再用
Copy -> 简单按位复制,原值还能用
Clone -> 显式复制,可能分配、可能很贵
简单值通常是 Copy
整数、布尔值、浮点数这类简单值,赋值和传参会复制:
let a: u32 = 10;
let b = a;
println!("{a}");
println!("{b}");
u32 实现了 Copy。赋值时,b 得到一份独立的值,a 仍然可用。
函数传参也是一样:
fn print_id(id: u32) {
println!("{id}");
}
let id = 42_u32;
print_id(id);
println!("{id}");
这和 C 里的整数传值很接近。
如果一个结构体的所有字段都是 Copy,也可以让结构体 Copy:
#[derive(Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
let p1 = Point { x: 1, y: 2 };
let p2 = p1;
println!("{}", p1.x);
Copy 类型的复制应该是便宜、直接、没有资源归属歧义的。
拥有资源的值默认 move
String、Vec<T>、File 这类值拥有资源。赋值不是复制资源,而是移动所有权:
let a = String::from("device");
let b = a;
// println!("{a}"); // 不能再用
println!("{b}");
这像是 C 里把一个拥有指针的结构体“转交”给另一个变量,而不是做深拷贝。Rust 让原变量失效,是为了避免两个值都以为自己拥有同一份资源。
Vec<u8> 同理:
let packet = vec![1_u8, 2, 3];
let moved = packet;
// packet 不能再用
println!("{}", moved.len());
传给函数也会 move:
fn consume(data: Vec<u8>) {
println!("{}", data.len());
}
let data = vec![1_u8, 2, 3];
consume(data);
// data 不能再用
如果函数只是读取,应该借用:
fn inspect(data: &[u8]) {
println!("{}", data.len());
}
let data = vec![1_u8, 2, 3];
inspect(&data);
println!("{}", data.len());
Clone 是显式复制
如果确实需要两份拥有的数据,用 clone():
let a = String::from("device");
let b = a.clone();
println!("{a}");
println!("{b}");
对 String 来说,clone() 会分配新内存并复制文本。对 Vec<T> 来说,clone() 会复制元素。
let a = vec![1_u8, 2, 3];
let b = a.clone();
这更接近 C 里的深拷贝:
uint8_t *copy = malloc(len);
memcpy(copy, src, len);
不要把 clone() 当成“让编译器闭嘴”的默认手段。它是一个真实操作,可能有分配和复制成本。
Copy 和 Clone 的关系
Copy 是一种更强的承诺:复制可以隐式发生,而且代价和语义都应该像简单值。
Clone 是显式复制:
let b = a.clone();
所有 Copy 类型也应该能 Clone,所以常见派生是:
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct DeviceId(u32);
这个 newtype 只是包了一层 u32,没有资源归属问题,适合 Copy。
但下面这种类型不应该 Copy:
struct Packet {
data: Vec<u8>,
}
因为复制 Packet 如果只是复制 Vec 的指针、长度、容量,会导致两个值指向同一块堆内存。Rust 不允许 Vec<u8> 实现 Copy,也就不允许这个 Packet 自动 Copy。
如果要复制 packet,必须显式:
#[derive(Clone)]
struct Packet {
data: Vec<u8>,
}
let p2 = p1.clone();
函数签名里的 Clone bound
泛型代码里,如果你要复制一个 T,必须要求 T: Clone:
fn duplicate<T: Clone>(value: T) -> (T, T) {
(value.clone(), value)
}
如果只需要复制简单值,可以要求 T: Copy:
fn duplicate_copy<T: Copy>(value: T) -> (T, T) {
(value, value)
}
这两个 bound 的含义不同:
T: Copy -> 可以隐式复制,适合简单值
T: Clone -> 可以显式复制,可能有成本
不要为了方便给泛型随便加 Clone。这会把“调用方的类型必须能复制”变成你的 API 要求。
C 结构体按值传参的迁移
C 里小结构体按值传参很自然:
struct Point {
int x;
int y;
};
void draw_point(struct Point p);
Rust 可以让它 Copy:
#[derive(Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn draw_point(p: Point) {
println!("{}, {}", p.x, p.y);
}
但如果结构体拥有堆内存:
struct Config {
name: String,
}
按值传参表示接管所有权:
fn set_config(config: Config) {
// 接管 config
}
如果只是读取,接收借用:
fn print_config(config: &Config) {
println!("{}", config.name);
}
这就是 Rust API 和 C API 的核心差别之一:按值传参不总是“便宜复制”,它可能表示所有权移动。
这一层先记住什么
C 到 Rust 的第一层对应关系:
整数/小 POD 按值复制 -> Copy
拥有 malloc 内存的结构体赋值 -> Rust 默认 move,避免双重释放
手写深拷贝 -> Clone / clone()
函数只是读取 -> 借用,不要 clone
泛型里需要复制 -> T: Clone 或 T: Copy
资源类型 Vec/String/File -> 默认 move,不是 Copy
move、Copy、Clone 不是三个孤立术语。它们都在回答同一个问题:这次赋值或传参,到底是转移所有权、便宜复制,还是显式复制一份资源。