Rust 中 "拷贝" 的本质
在 Rust 中, 一个值从变量 A 赋值/传递到变量 B 时, 底层到底发生了什么, 取决于这个值的类型, 一共只有三种情况:
- move(转移所有权): 直接把所有权移交, 原变量失效, 之后不能再使用
- copy(浅拷贝): 将栈上的数据按位复制一份, 原变量仍然有效
- clone(克隆): 显式调用
.clone()方法, 连堆上的数据一起深拷贝一份
一个值赋值时到底是 move 还是 copy, 由它是否实现了 Copy trait 决定, 与数据本身的大小无关
先区分: 栈上的数据 和 堆上的数据
在讨论拷贝之前, 先回顾一下内存模型, 数据在内存中有两种存放方式
| 存放位置 | 特点 | 典型类型 |
|---|---|---|
| 栈 | 大小固定, 编译期就能确定, 读写极快 | 整数/浮点/布尔/字符/引用 |
| 堆 | 大小不固定, 运行时动态分配 | String/Vec/Box 等(含指针结构) |
fn main() {
// 栈上的数据: 大小固定, 直接存在栈上
let x: i32 = 10;
// 堆上的数据: "hello" 存在堆上, 栈上只保存(指针/长度/容量)
let s = String::from("hello");
}可以用 std::mem::size_of 查看一个类型占用的栈空间大小, 注意它只能测出栈上的部分
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 (转移所有权), 而不是拷贝
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 的类型(整数/浮点/布尔等), 赋值时直接复制一份, 原变量仍然有效
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 |
| bool | true/false |
| char | 单个 Unicode 字符 |
| 元组/数组 | 要求里面每个元素都实现了 Copy |
| &T 不可变引用 | 复制一个引用(指针), 注意 &mut T 不行 |
为什么叫"浅拷贝"?
Copy 只复制 栈上 的内容。对于纯栈类型(整数/布尔), 复制栈就等于复制了一切; 对于引用 &T, 复制的是引用本身, 不复制指向的数据, 所以叫"浅拷贝"
clone:克隆(深拷贝)
想要真正复制一份包含堆数据的独立副本, 必须显式调用 .clone() 方法
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 | 不需要 |
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() 只是复制了一个引用(指针), 并没有复制任何堆数据
fn main() {
// 想得到 String, 正确的写法是:
let s1 = String::from("test"); // 方式一: 从 &str 构造
let s2 = "test".to_string(); // 方式二: 转换成 String
println!("{s1}, {s2}");
}克隆与拷贝接口核心区别
Copy trait
Copy 是一个标记 trait(marker trait), 它里面没有任何方法, 只是一个"标签", 告诉编译器这个类型可以安全地按位复制
// 标准库中的定义(简化)
pub trait Copy: Clone {}- 实现方式: 用
#[derive(Copy)]自动实现, 或者手动impl Copy for T {} - 限制: 类型的所有字段必须都实现了 Copy, 才能实现 Copy
- 只要实现了 Copy, 赋值/传参时会自动按位复制, 无法自定义复制逻辑
Clone trait
Clone 是一个普通 trait, 它定义了一个 clone() 方法, 负责创建一份"独立副本"
// 标准库中的定义(简化)
pub trait Clone {
fn clone(&self) -> Self; // 必须实现的方法
fn clone_from(&mut self, source: &Self) { ... } // 有默认实现, 可覆盖
}- 实现方式: 用
#[derive(Clone)]自动实现, 或者手动实现clone()方法 - 调用方式: 必须显式调用
.clone(), 编译器不会自动克隆 - 可以自定义复制逻辑(比如只复制部分字段)
核心区别对比
| 对比项 | Copy | Clone |
|---|---|---|
| 类型 | 标记 trait(无方法) | 普通 trait(有 clone() 方法) |
| 触发方式 | 赋值/传参时自动发生 | 必须显式调用 .clone() |
| 复制深度 | 只复制栈上的内容(浅拷贝) | 深拷贝(含堆数据) |
| 能否自定义逻辑 | 不能, 就是按位复制 | 可以, 在 clone() 里自己写 |
| 实现限制 | 所有字段必须都实现 Copy | 没有限制, 任何类型都能实现 |
| 运行开销 | 零成本 | 可能分配堆内存, 有开销 |
| 依赖关系 | Copy: Clone, 实现 Copy 必须先实现 Clone | 不依赖 Copy |
自动派生宏 #[derive(Clone, Copy)]
最常用的方式是直接派生出这两个 trait:
#[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);
}注意点
- 派生
Copy的前提: 所有字段都实现了 Copy, 包含String/Vec等堆数据的结构体, 不能派生 Copy(编译报错) Copy: Clone, 所以写了#[derive(Clone, Copy)]时, 编译器会同时生成两个 trait 的实现- 只想深拷贝不想浅拷贝? 只写
#[derive(Clone)]就够了
// 一行搞定, 自动实现 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:?}");
}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() |
// 推荐: 参数传引用, 不拷贝
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
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
// 推荐: 直接返回所有权, 编译器会做优化, 零拷贝
fn make_string() -> String {
let s = String::from("hello");
s // 直接返回, 不要写 s.clone()
}
fn main() {
let s = make_string();
println!("{s}"); // hello
}5. 实现 Copy 之前先问自己三个问题
- 这个类型的所有字段都实现了 Copy 吗?(含 String/Vec 就不行)
- 复制它真的"零成本"吗?(如果内部有引用指向堆数据, 浅拷贝可能不是想要的行为)
- 我真的需要"赋值后两边都能用"吗?(很多时候传引用就够了)
总结
- move: 默认行为, 堆数据类型赋值就是转移所有权, 原变量失效
- copy: 实现了 Copy 的类型(整数/浮点/布尔等), 赋值自动浅拷贝, 零成本
- clone: 显式调用
.clone(), 深拷贝, 有开销, 按需使用 - 判断标准: 一个类型赋值时是 move 还是 copy, 看它有没有实现 Copy trait