Rust Copy详解:隐式复制与轻量数据类型

《Rust编程实战》系列第34篇

在上一篇文章中,我们学习了Rust的Clone,了解了如何通过:

php 复制代码
value.clone()

显式创建一个值的副本。

对于StringVecHashMap等拥有堆数据的类型,clone()通常会复制底层数据,因此可能产生额外的内存分配和性能开销。

本篇继续学习Rust所有权体系中的另一个重要Trait:

rust 复制代码
Copy

Copy与Clone都和"复制"有关,但它们的使用方式和适用类型完全不同。

例如:

ini 复制代码
fn main() {
    let first = 100;
    let second = first;
    println!("{}", first);
    println!("{}", second);
}

这里虽然执行了:

ini 复制代码
let second = first;

first并没有失效。

原因是:

rust 复制代码
i32

实现了Copy Trait,因此赋值时发生的是隐式复制,而不是所有权移动。

本文将介绍:

  • 什么是Copy

  • Copy与Move的区别

  • Copy与Clone的关系

  • 哪些类型实现Copy

  • 元组和数组的Copy规则

  • 结构体实现Copy

  • #[derive(Copy, Clone)]

  • 为什么String不能Copy

  • Copy在函数参数中的行为

  • Copy在模式匹配中的行为

  • 什么时候应该实现Copy

  • 什么时候不应该实现Copy

什么是Copy

Copy是Rust标准库中的一个标记Trait。

当一个类型实现Copy后,变量赋值时不会发生所有权移动,而是自动复制值。

例如:

ini 复制代码
fn main() {
    let first: i32 = 100;
    let second = first;
    println!("first = {}", first);
    println!("second = {}", second);
}

输出:

ini 复制代码
first = 100
second = 100

这里:

ini 复制代码
let second = first;

相当于复制了first中的数据。

执行后:

sql 复制代码
first 有效
second 有效

而如果是String

ini 复制代码
fn main() {
    let first = String::from("Rust");
    let second = first;
    println!("{}", second);
}

执行后:

sql 复制代码
first 失效
second 有效

因为String没有实现Copy。

Copy与Move的区别

Move表示转移所有权:

ini 复制代码
let first = String::from("Rust");
let second = first;

结果:

sql 复制代码
first失效
second成为所有者

Copy表示复制值:

ini 复制代码
let first = 100;
let second = first;

结果:

sql 复制代码
first有效
second有效

可以简单记住:

Move是把原来的值交出去。

Copy是保留原值,再复制一份新值。

为什么需要Copy

对于整数、布尔值这样的简单数据,如果每次赋值都发生Move,会让代码非常难写。

例如:

ini 复制代码
let age = 20;
let new_age = age;

如果age立即失效,那么简单数字在函数之间传递也需要频繁借用。

实际上,复制一个:

rust 复制代码
i32

只需要复制几个字节,成本很低。

因此Rust允许这类轻量类型实现Copy,让使用体验更加自然。

常见Copy类型

Rust中很多基础类型都实现了Copy。

整数类型

例如:

rust 复制代码
i8
i16
i32
i64
i128
isize
u8
u16
u32
u64
u128
usize

示例:

ini 复制代码
let a = 10u32;
let b = a;
println!("{} {}", a, b);

浮点类型

rust 复制代码
f32
f64

示例:

ini 复制代码
let price = 99.9;
let new_price = price;
println!("{} {}", price, new_price);

布尔类型

arduino 复制代码
bool
ini 复制代码
let enabled = true;
let status = enabled;
println!("{} {}", enabled, status);

字符类型

arduino 复制代码
char
ini 复制代码
let letter = 'R';
let copy = letter;
println!("{} {}", letter, copy);

不可变引用

普通共享引用:

r 复制代码
&T

通常实现Copy。

例如:

ini 复制代码
fn main() {
    let text = String::from("Rust");
    let first = &text;
    let second = first;
    println!("{}", first);
    println!("{}", second);
}

这里复制的是引用本身,而不是复制String数据。

Copy与Clone的关系

Copy和Clone并不是互相独立的。

Rust规定:

一个类型如果实现Copy,也必须实现Clone。

因此:

rust 复制代码
Copy: Clone

可以理解为Copy建立在Clone之上。

对于Copy类型:

ini 复制代码
let a = 10;
let b = a;

是隐式复制。

也可以显式调用:

ini 复制代码
let b = a.clone();

例如:

ini 复制代码
fn main() {
    let a = 100;
    let b = a.clone();
    println!("{} {}", a, b);
}

虽然可以这样写,但对于i32这类Copy类型,一般没有必要显式调用clone()

直接:

ini 复制代码
let b = a;

更自然。

Copy不是深拷贝

Copy通常只是按位复制一个值。

例如:

ini 复制代码
let a: i32 = 100;
let b = a;

复制的是这个整数本身。

对于引用:

ini 复制代码
let first = &text;
let second = first;

复制的只是引用地址。

此时:

sql 复制代码
first ─┐
       ├──> 同一个text
second ┘

底层String并没有复制。

所以:

Copy只表示复制当前值成本低且语义安全,并不表示复制它引用的所有底层数据。

元组什么时候实现Copy

元组是否实现Copy,取决于所有元素是否都实现Copy。

例如:

ini 复制代码
fn main() {
    let first = (10, 20.5, true);
    let second = first;
    println!("{:?}", first);
    println!("{:?}", second);
}

这里:

  • i32实现Copy

  • f64实现Copy

  • bool实现Copy

    所以整个元组也可以Copy。

    但如果元组包含String

rust 复制代码
fn main() {
    let first = (
        1,
        String::from("Tom"),
    );
    let second = first;
    println!("{:?}", second);
}

这时整个元组不能Copy,因为其中的String不能Copy。

因此first发生Move。

数组什么时候实现Copy

数组也遵循类似规则。

例如:

ini 复制代码
fn main() {
    let first = [1, 2, 3];
    let second = first;
    println!("{:?}", first);
    println!("{:?}", second);
}

数组元素都是i32,因此数组可以Copy。

如果数组包含String

rust 复制代码
fn main() {
    let first = [
        String::from("A"),
        String::from("B"),
    ];
    let second = first;
    println!("{:?}", second);
}

此时发生Move,不能继续整体使用first

结构体默认不会实现Copy

自定义结构体默认没有Copy。

例如:

rust 复制代码
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let first = Point {
        x: 10,
        y: 20,
    };
    let second = first;
    println!("{:?}", second);
}

此时first被移动。

即使字段全部是i32,Rust也不会自动让自定义结构体实现Copy。

如果需要,可以显式添加:

bash 复制代码
#[derive(Copy, Clone)]

结构体实现Copy

示例:

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

fn main() {
    let first = Point {
        x: 10,
        y: 20,
    };
    let second = first;
    println!("{:?}", first);
    println!("{:?}", second);
}

现在两个变量都可以使用。

注意:

bash 复制代码
#[derive(Copy, Clone)]

通常两个一起写。

因为实现Copy的类型必须同时实现Clone。

结构体实现Copy的条件

结构体中的所有字段都必须实现Copy。

下面可以:

rust 复制代码
#[derive(Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

因为两个字段都是Copy类型。

下面不可以:

rust 复制代码
#[derive(Copy, Clone)]
struct User {
    name: String,
    age: u32,
}

因为:

arduino 复制代码
String

没有实现Copy。

编译器会直接拒绝。

为什么String不能Copy

这是Copy最重要的问题之一。

String内部通常保存:

复制代码
指针
长度
容量

真正文本位于堆内存。

假设String可以Copy:

ini 复制代码
let first = String::from("Rust");
let second = first;

如果只是复制指针、长度和容量,那么:

sql 复制代码
first  ─┐
        ├──> 同一块Rust堆内存
second ─┘

当作用域结束时:

sql 复制代码
first释放一次
second再次释放

会造成double free。

如果Copy时完整复制堆数据,又会产生隐藏的昂贵内存操作。

Rust的设计原则是:

Copy应该是低成本、可预测的隐式复制。

因此String不能实现Copy,而需要显式:

scss 复制代码
clone()

这样代码能够明确告诉读者:

这里可能发生真正的数据复制。

Vec为什么不能Copy

Vec<T>String类似,也管理堆内存:

复制代码
指针
长度
容量

因此:

ini 复制代码
let first = vec![1, 2, 3];
let second = first;

发生Move。

如果需要两个独立Vec:

ini 复制代码
let second = first.clone();

Copy在函数参数中的行为

Copy类型传给函数时,原变量仍然有效。

例如:

rust 复制代码
fn print_age(age: u32) {
    println!("年龄:{}", age);
}

fn main() {
    let age = 20;
    print_age(age);
    println!("main中的age:{}", age);
}

调用:

scss 复制代码
print_age(age);

时传入的是age的副本。

函数内部参数销毁不会影响原变量。

非Copy类型作为函数参数

如果参数是String

javascript 复制代码
fn print_name(name: String) {
    println!("{}", name);
}

fn main() {
    let name = String::from("Tom");
    print_name(name);
}

所有权会Move到函数中。

之后不能继续使用:

arduino 复制代码
println!("{}", name);

如果函数只需要读取,可以改为:

rust 复制代码
fn print_name(name: &str) {
    println!("{}", name);
}

这就进入下一篇要讲的Borrow借用。

Copy在函数返回值中的行为

返回Copy类型时,本质上得到一个独立副本:

rust 复制代码
fn get_score() -> i32 {
    let score = 100;
    score
}

fn main() {
    let score = get_score();
    println!("{}", score);
}

对于Copy类型,不需要考虑堆资源所有权转移问题。

Copy与模式匹配

Copy类型在模式匹配中通常不会导致原值失效。

rust 复制代码
fn main() {
    let value = Some(100);
    match value {
        Some(number) => {
            println!("{}", number);
        }
        None => {}
    }
    println!("{:?}", value);
}

Option<i32>本身可以Copy,因此匹配后value仍然可以使用。

如果是:

rust 复制代码
Option<String>

情况就不同:

rust 复制代码
fn main() {
    let value = Some(String::from("Rust"));
    match value {
        Some(text) => {
            println!("{}", text);
        }
        None => {}
    }
}

内部String会被移动。

如果后面还要使用value,可以使用引用匹配:

arduino 复制代码
match &value {
    Some(text) => println!("{}", text),
    None => {}
}

Copy与闭包

Copy类型被闭包捕获时,也通常更简单。

例如:

ini 复制代码
fn main() {
    let number = 100;
    let show = move || {
        println!("{}", number);
    };
    show();
    println!("{}", number);
}

即使使用了:

arduino 复制代码
move

由于number实现Copy,闭包得到的是副本,外部的number仍然有效。

如果是Stringmove闭包通常会接管所有权。

手动实现Copy

Copy Trait本身没有需要实现的方法:

kotlin 复制代码
trait Copy: Clone {}

因此可以手动实现:

rust 复制代码
#[derive(Clone)]
struct Point {
    x: i32,
    y: i32,
}

impl Copy for Point {}

不过实际开发中更推荐:

bash 复制代码
#[derive(Copy, Clone)]

更加简洁,也不容易出错。

什么时候适合实现Copy

适合Copy的类型通常具有以下特点:

  • 数据量很小

  • 没有管理堆资源

  • 复制成本很低

  • 复制后两个值完全独立

    例如:

rust 复制代码
#[derive(Debug, Copy, Clone)]
struct Position {
    x: i32,
    y: i32,
}

坐标非常适合Copy。

又例如:

rust 复制代码
#[derive(Debug, Copy, Clone)]
struct Size {
    width: u32,
    height: u32,
}

这种轻量值对象也适合Copy。

枚举实现Copy

简单枚举也非常适合Copy:

csharp 复制代码
#[derive(Debug, Copy, Clone)]
enum Status {
    Pending,
    Running,
    Finished,
}

示例:

ini 复制代码
fn main() {
    let first = Status::Running;
    let second = first;
    println!("{:?}", first);
    println!("{:?}", second);
}

如果枚举变体包含的所有数据都是Copy,也可以实现Copy:

scss 复制代码
#[derive(Debug, Copy, Clone)]
enum Value {
    Integer(i32),
    Boolean(bool),
}

什么时候不应该实现Copy

管理堆资源

例如:

rust 复制代码
String
Vec<T>
HashMap<K,V>

不应该Copy。

对象复制成本较高

如果结构体非常大,即使字段都可以Copy,也不一定需要实现Copy。

类型有明确唯一身份

例如:

复制代码
数据库连接
文件句柄
锁
网络连接
事务

这种资源通常不应该产生隐式副本。

希望显式体现复制操作

某些业务对象即使可以复制,也希望开发者明确调用:

scss 复制代码
clone()

这样代码语义更清楚。

Copy和Clone应该怎么选

如果类型非常轻量:

复制代码
Point
Size
Status

可以考虑:

rust 复制代码
Copy + Clone

如果类型拥有堆资源:

rust 复制代码
String
Vec

通常:

rust 复制代码
Clone

但不能Copy。

如果类型根本不应该被复制:

复制代码
文件
连接
锁
唯一资源

可以:

rust 复制代码
既不Copy,也不Clone

实战:商品尺寸

arduino 复制代码
#[derive(Debug, Copy, Clone)]
struct Size {
    width: f64,
    height: f64,
}

fn calculate_area(size: Size) -> f64 {
    size.width * size.height
}

fn main() {
    let size = Size {
        width: 10.0,
        height: 20.0,
    };
    let area = calculate_area(size);
    println!("面积:{}", area);
    println!("尺寸:{:?}", size);
}

因为Size实现了Copy,传入函数后仍然可以继续使用原变量。

这类小型值对象使用Copy非常方便。

实战:订单状态

rust 复制代码
#[derive(Debug, Copy, Clone, PartialEq)]
enum OrderStatus {
    Pending,
    Paid,
    Shipped,
    Finished,
}

fn is_finished(status: OrderStatus) -> bool {
    status == OrderStatus::Finished
}

fn main() {
    let status = OrderStatus::Paid;
    let result = is_finished(status);
    println!("是否完成:{}", result);
    println!("当前状态:{:?}", status);
}

状态枚举通常数据很小,非常适合Copy。

常见错误

误以为所有赋值都会Copy

错误理解:

ini 复制代码
let second = first;

总是复制。

实际上:

  • Copy类型:复制

  • 非Copy类型:Move

尝试让String结构体实现Copy

错误:

rust 复制代码
#[derive(Copy, Clone)]
struct User {
    name: String,
}

无法通过编译。

为大型对象随意实现Copy

Copy是隐式发生的,如果对象很大,会让复制成本变得不明显。

把Copy和clone混为一谈

Copy:

ini 复制代码
let b = a;

隐式。

Clone:

ini 复制代码
let b = a.clone();

显式。

认为引用Copy会复制底层数据

ini 复制代码
let b = a_ref;

通常只是复制引用,并不会复制原对象。

Copy最佳实践

小型值对象优先考虑Copy

例如坐标、尺寸、状态、ID包装类型。

有堆资源时使用Clone或Move

例如:

rust 复制代码
String
Vec<T>

不要试图实现Copy。

不要为了方便而实现Copy

应该根据类型语义决定,而不是为了减少所有权编译错误。

复杂对象优先借用

如果函数只是读取对象:

php 复制代码
fn print_user(user: &User)

通常比复制整个对象更合理。

明确区分值语义和资源语义

例如:

css 复制代码
Position

更像"值",适合Copy。

复制代码
DatabaseConnection

更像"资源",不适合Copy。

本章小结

Copy是Rust所有权系统中用于轻量数据隐式复制的重要Trait。

本文学习了:

  • Copy类型赋值不会发生Move

  • Copy是一种隐式复制

  • 实现Copy的类型必须同时实现Clone

  • 整数、浮点数、bool、char通常实现Copy

  • 共享引用通常可以Copy

  • 元组和数组只有在内部元素全部Copy时才能Copy

  • 自定义结构体可以使用#[derive(Copy, Clone)]

  • String、Vec等拥有堆资源的类型不能Copy

  • Copy类型传入函数后原变量仍然有效

  • Copy类型在模式匹配和move闭包中的行为

  • 小型值对象和状态枚举适合Copy

  • 资源类型和大型对象通常不适合Copy

    可以记住:

Move转移所有权。

Clone显式创建副本。

Copy隐式创建轻量副本。

Copy不是为了绕过所有权,而是用于复制成本低、复制语义明确的数据类型。

下一篇预告

下一篇我们将正式学习Rust所有权系统中另一个核心概念:

Rust Borrow借用详解:不转移所有权访问数据

内容包括:

  • 什么是Borrow

  • 为什么需要借用

  • 引用&T

  • 可变引用&mut T

  • 借用与Move的区别

  • 借用规则

  • 多个不可变引用

  • 唯一可变引用

  • 可变与不可变借用冲突

  • 函数参数中的Borrow

  • Dereference解引用

  • 借用与String、Vec、Struct

  • 借用检查器Borrow Checker

  • 常见编译错误与实战案例

相关推荐
仙人球部落 揞殺2 小时前
细说ASP.NET的各种异步操作
后端·asp.net
kaixin_learn_qt_ing2 小时前
了解Rust/Tauri
rust
四六的六3 小时前
端侧模型多端部署实战:从格式转换到灰度发布,Web 和移动端统一部署流水线
前端·人工智能·大模型·ai编程·ai模型·ai产品·端侧ai
卷无止境4 小时前
当独立开发者也能造出3A画质的游戏:Godot引擎深度解析
后端·python·godot
doiito4 小时前
【Agent Harness】Gliding Horse 最新升级:把“智能闭环”真正跑通的一次深度优化
rust·架构设计·系统设计·ai agent
独隅5 小时前
前端离线暂停更新策略:Service Worker 与 PWA 的静默控制实践
前端
独行侠影a5 小时前
SpringBoot 分布式锁实战:Redisson 解决订单超卖并发问题
spring boot·分布式·后端
妙码生花5 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十八):后台系统配置管理实现
前端·后端·go