Skip to content

Rust 中 "拷贝" 的本质

在 Rust 中, 一个值从变量 A 赋值/传递到变量 B 时, 底层到底发生了什么, 取决于这个值的类型, 一共只有三种情况:

  • move(转移所有权): 直接把所有权移交, 原变量失效, 之后不能再使用
  • copy(浅拷贝): 将栈上的数据按位复制一份, 原变量仍然有效
  • clone(克隆): 显式调用 .clone() 方法, 连堆上的数据一起深拷贝一份

一个值赋值时到底是 move 还是 copy, 由它是否实现了 Copy trait 决定, 与数据本身的大小无关

先区分: 栈上的数据 和 堆上的数据

在讨论拷贝之前, 先回顾一下内存模型, 数据在内存中有两种存放方式

存放位置特点典型类型
大小固定, 编译期就能确定, 读写极快整数/浮点/布尔/字符/引用
大小不固定, 运行时动态分配String/Vec/Box 等(含指针结构)
rust
fn main() {
    // 栈上的数据: 大小固定, 直接存在栈上
    let x: i32 = 10;

    // 堆上的数据: "hello" 存在堆上, 栈上只保存(指针/长度/容量)
    let s = String::from("hello");
}

可以用 std::mem::size_of 查看一个类型占用的栈空间大小, 注意它只能测出栈上的部分

rust
fn main() {
    use std::mem::size_of;

    // 栈上的数据, 大小固定
    println!("i32:   {} 字节", size_of::<i32>());   // 4
    println!("bool:  {} 字节", size_of::<bool>());  // 1

    // String 在栈上只是一个(指针/长度/容量)的结构体
    println!("String: {} 字节", size_of::<String>()); // 24
}

move: 转移所有权

对于没有实现 Copy 的类型(如 String), 赋值/传参时发生的是 move (转移所有权), 而不是拷贝

rust
fn main() {
    // 栈内存中的数据, 赋值是直接 Copy 一份新的
    // 栈内存中的数据, 大小是固定的
    let n1: i32 = 10;
    let n2 = n1;
    println!("n1 is {}", n1);
    println!("n2 is {}", n2);

    // 堆内存中的数据, 是 Move 所有权, 并不会 Copy 一份新的
    // 堆内存中的数据, 大小是可扩展的, 可以动态 push_str 追加
    // 此时, 就需要重新分配内存
    let s1: String = String::from("Hello");
    let s2 = s1;
    // 所有权已经移交给 s2, 所以此处会报错
    // println!("s1 is {}", s1);
    //                   ^^ value borrowed here after move
    println!("s2 is {}", s2);
}

注意点

为什么 String 赋值不复制数据?

  • 为了速度, 直接赋值只能复制指针(不能真的复制堆上的数据)
  • 如果 s1/s2 都指向同一块堆内存, 程序结束时会被释放两次(double free)
  • 所以 Rust 选择: 移交所有权 + 让原变量失效, 从根上杜绝这个问题

copy: 浅拷贝

对于 实现了 Copy 的类型(整数/浮点/布尔等), 赋值时直接复制一份, 原变量仍然有效

rust
fn main() {
    let a = 10;
    let b = a; // i32 实现了 Copy, 直接把值复制一份

    // a 和 b 都能使用
    println!("a = {a}, b = {b}"); // a = 10, b = 10

    // 因为栈内存中的数据大小固定的, 复制一份非常快
    // 所以, 为了速度考虑, 不需要重新分配堆内存
}

所有实现了 Copy 的常用类型:

类型说明
所有整数类型i8/i16/i32/i64/isize/u8/u16/u32/u64/usize
所有浮点类型f32/f64
booltrue/false
char单个 Unicode 字符
元组/数组要求里面每个元素都实现了 Copy
&T 不可变引用复制一个引用(指针), 注意 &mut T 不行

为什么叫"浅拷贝"?

Copy 只复制 栈上 的内容。对于纯栈类型(整数/布尔), 复制栈就等于复制了一切; 对于引用 &T, 复制的是引用本身, 不复制指向的数据, 所以叫"浅拷贝"

clone:克隆(深拷贝)

想要真正复制一份包含堆数据的独立副本, 必须显式调用 .clone() 方法

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // 重新申请堆内存, 完整复制一份

    // s1 和 s2 各自拥有独立的堆内存, 互不影响
    println!("s1 = {s1}, s2 = {s2}");
}

注意点

  • clone 需要类型实现了 Clone trait
  • clone 是深拷贝, 会分配新的堆内存, 有运行时开销, 不能乱用
  • 不是所有类型都实现了 Clone, 自定义类型需要手动实现或 derive

三种方式对比

方式触发条件栈上的数据堆上的数据原变量还能用吗
move类型没实现 Copy, 直接赋值复制指针不复制不能用
copy类型实现了 Copy, 直接赋值按位复制无堆数据能用
clone显式调用 .clone()按位复制完整复制能用

字符串操作注意

&str 转换成 String, 有三种常见写法, 它们的结果并不完全一样:

  • "test".into()
  • "test".to_string()
  • "test".clone()
写法结果类型原理是否需要标注类型
"test".into()看标注通过 From/Into, 编译期无法推断, 必须标注目标类型必须
"test".to_string()String通过 ToString, 调用的是 Display 的格式化逻辑不需要
"test".clone()&str字符串字面量本身是 &'static str, 调用的是引用的 Clone不需要
rust
fn main() {
    // 1. into(): 必须标注目标类型, 否则编译器不知道转成什么
    let s1: String = "test".into();
    println!("{s1}"); // test

    // 2. to_string(): 结果确定是 String, 不需要标注
    let s2 = "test".to_string();
    println!("{s2}"); // test

    // 3. clone(): 结果仍然是 &str, 不是 String!
    let s3 = "test".clone();
    // let s3: String = "test".clone(); // 编译报错: 类型不匹配
    println!("{s3}"); // test
}

注意点

"test".clone() 得到的是 &str 而不是 String

因为字符串字面量 "test" 的类型是 &'static str, 而引用类型 &str 本身实现了 Copy + Clone, 所以 .clone() 只是复制了一个引用(指针), 并没有复制任何堆数据

rust
fn main() {
    // 想得到 String, 正确的写法是:
    let s1 = String::from("test");  // 方式一: 从 &str 构造
    let s2 = "test".to_string();    // 方式二: 转换成 String
    println!("{s1}, {s2}");
}

克隆与拷贝接口核心区别

Copy trait

Copy 是一个标记 trait(marker trait), 它里面没有任何方法, 只是一个"标签", 告诉编译器这个类型可以安全地按位复制

rust
// 标准库中的定义(简化)
pub trait Copy: Clone {}
  • 实现方式: 用 #[derive(Copy)] 自动实现, 或者手动 impl Copy for T {}
  • 限制: 类型的所有字段必须都实现了 Copy, 才能实现 Copy
  • 只要实现了 Copy, 赋值/传参时会自动按位复制, 无法自定义复制逻辑

Clone trait

Clone 是一个普通 trait, 它定义了一个 clone() 方法, 负责创建一份"独立副本"

rust
// 标准库中的定义(简化)
pub trait Clone {
    fn clone(&self) -> Self; // 必须实现的方法
    fn clone_from(&mut self, source: &Self) { ... } // 有默认实现, 可覆盖
}
  • 实现方式: 用 #[derive(Clone)] 自动实现, 或者手动实现 clone() 方法
  • 调用方式: 必须显式调用 .clone(), 编译器不会自动克隆
  • 可以自定义复制逻辑(比如只复制部分字段)

核心区别对比

对比项CopyClone
类型标记 trait(无方法)普通 trait(有 clone() 方法)
触发方式赋值/传参时自动发生必须显式调用 .clone()
复制深度只复制栈上的内容(浅拷贝)深拷贝(含堆数据)
能否自定义逻辑不能, 就是按位复制可以, 在 clone() 里自己写
实现限制所有字段必须都实现 Copy没有限制, 任何类型都能实现
运行开销零成本可能分配堆内存, 有开销
依赖关系Copy: Clone, 实现 Copy 必须先实现 Clone不依赖 Copy

自动派生宏 #[derive(Clone, Copy)]

最常用的方式是直接派生出这两个 trait:

rust
#[derive(Debug, Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };

    // 实现了 Copy, 赋值后 p1 仍然能用
    let p2 = p1;
    println!("p1 = {:?}, p2 = {:?}", p1, p2);

    // 也可以显式调用 clone()
    let p3 = p1.clone();
    println!("p3 = {:?}", p3);
}

注意点

  1. 派生 Copy 的前提: 所有字段都实现了 Copy, 包含 String/Vec 等堆数据的结构体, 不能派生 Copy(编译报错)
  2. Copy: Clone, 所以写了 #[derive(Clone, Copy)] 时, 编译器会同时生成两个 trait 的实现
  3. 只想深拷贝不想浅拷贝? 只写 #[derive(Clone)] 就够了
rust
// 一行搞定, 自动实现 Copy + Clone
#[derive(Debug, Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1; // Copy: 自动按位复制
    let p3 = p1.clone(); // Clone: 显式克隆

    println!("p1 = {p1:?}, p2 = {p2:?}, p3 = {p3:?}");
}
rust
struct Point {
    x: i32,
    y: i32,
}

// 手动实现 Clone
impl Clone for Point {
    fn clone(&self) -> Self {
        // 默认逻辑就是逐个字段复制, 和 derive 展开的结果一样
        Point {
            x: self.x,
            y: self.y,
        }
    }
}

// 手动实现 Copy: 没有任何方法, 只是个标记
impl Copy for Point {}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1; // Copy: 自动按位复制
    let p3 = p1.clone(); // Clone: 显式克隆
    println!("p1.x = {}, p2.x = {}, p3.x = {}", p1.x, p2.x, p3.x);
}

实际开发建议

1. 小数据类型用 Copy, 大数据类型用借用或 Clone

场景建议
只含整数/浮点/布尔等小字段的结构体实现 Copy + Clone, 用着方便
包含 String/Vec 等堆数据的结构体不要 Copy, 传引用 &T, 必要时 Clone
函数参数只是"看一眼"数据&T&str, 零拷贝
函数需要一份独立的可修改数据&T 后内部 .clone()
rust
// 推荐: 参数传引用, 不拷贝
fn get_len(s: &str) -> usize {
    s.len()
}

// 不推荐: 传 String 会 move / 传克隆会白花钱
// fn get_len(s: String) -> usize { s.len() }

fn main() {
    let s = String::from("hello");
    println!("len = {}", get_len(&s)); // len = 5
    println!("{s}"); // s 还能用, 因为只借了引用
}

2. 按需 Clone, 不要无脑 Clone

rust
fn main() {
    let s = String::from("hello");

    // 场景 A: 只读数据 -> 用引用, 不要 clone
    let len = s.len();
    println!("{len}"); // 5

    // 场景 B: 真的需要一份独立副本 -> 才 clone
    let copy = s.clone();
    println!("{s}, {copy}");
}

3. 字符串转换, 用 to_string() 而不是 clone()

  • "test".to_string() 才是真正创建新的 String
  • "test".clone() 只复制了引用, 结果还是 &str, 容易踩坑

4. 返回值优先返回所有权, 不要 clone

rust
// 推荐: 直接返回所有权, 编译器会做优化, 零拷贝
fn make_string() -> String {
    let s = String::from("hello");
    s // 直接返回, 不要写 s.clone()
}

fn main() {
    let s = make_string();
    println!("{s}"); // hello
}

5. 实现 Copy 之前先问自己三个问题

  1. 这个类型的所有字段都实现了 Copy 吗?(含 String/Vec 就不行)
  2. 复制它真的"零成本"吗?(如果内部有引用指向堆数据, 浅拷贝可能不是想要的行为)
  3. 我真的需要"赋值后两边都能用"吗?(很多时候传引用就够了)

总结

  • move: 默认行为, 堆数据类型赋值就是转移所有权, 原变量失效
  • copy: 实现了 Copy 的类型(整数/浮点/布尔等), 赋值自动浅拷贝, 零成本
  • clone: 显式调用 .clone(), 深拷贝, 有开销, 按需使用
  • 判断标准: 一个类型赋值时是 move 还是 copy, 看它有没有实现 Copy trait

Released under the MIT License.