跳转到主内容移动、复制和 clone | 物联网民工

移动、复制和 clone

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 不是三个孤立术语。它们都在回答同一个问题:这次赋值或传参,到底是转移所有权、便宜复制,还是显式复制一份资源。

继续阅读