Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

English Original

避免过度使用 clone()

你将学到: 为什么在 Rust 中使用 .clone() 可能是一种“代码异味 (Code Smell)”、如何重构所有权以消除不必要的拷贝,以及哪些特定模式暗示了所有权设计存在问题。

  • 对于从 C++ 转来的开发者,.clone() 往往感觉像是一个稳妥的默认选项 —— “直接拷贝一份就行”。然而,过度克隆会掩盖所有权设计上的缺陷,并损害程序性能。
  • 经验法则:如果你克隆是为了取悦借用检查器,那么你可能需要重构所有权结构,而不是直接克隆。

何时使用 clone() 是错误的

#![allow(unused)]
fn main() {
// 错误写法:克隆一个 String,仅仅是为了将其传递给一个只需读取它的函数
fn log_message(msg: String) {  // 不必要地夺取了所有权
    println!("[LOG] {}", msg);
}
let message = String::from("GPU 测试通过");
log_message(message.clone());  // 浪费:分配了一个全新的 String
log_message(message);           // 原始值被消耗 —— 前面的克隆毫无意义
}
#![allow(unused)]
fn main() {
// 正确写法:接受一个借用 —— 零分配
fn log_message(msg: &str) {    // 借用,而不拥有
    println!("[LOG] {}", msg);
}
let message = String::from("GPU 测试通过");
log_message(&message);          // 无克隆,无分配
---

## 真实示例:返回 `&str` 而非克隆
```rust
// 示例:healthcheck.rs —— 返回借用的视图,零分配
pub fn serial_or_unknown(&self) -> &str {
    self.serial.as_deref().unwrap_or(UNKNOWN_VALUE)
}

pub fn model_or_unknown(&self) -> &str {
    self.model.as_deref().unwrap_or(UNKNOWN_VALUE)
}
}

在 C++ 中,等效的操作通常会返回 const std::string& 或 std::string_view —— 然而在 C++ 中,这两者都没有经过生命周期检查。而在 Rust 中,借用检查器保证了返回的 &str 绝不会比 self 存活得更久。

真实示例:静态字符串切片 —— 彻底避免堆分配

#![allow(unused)]
fn main() {
// 示例:healthcheck.rs —— 编译期字符串表
const HBM_SCREEN_RECIPES: &[&str] = &[
    "hbm_ds_ntd", "hbm_ds_ntd_gfx", "hbm_dt_ntd", "hbm_dt_ntd_gfx",
    "hbm_burnin_8h", "hbm_burnin_24h",
];
}

在 C++ 中,这通常会是 std::vector<std::string>(在第一次使用时在堆中分配内存)。而在 Rust 中,&'static [&'static str] 存储在只读内存中 —— 运行时开销为零。

何时 clone() 确实 适用

场景为什么克隆是可以接受的示例
多线程中的 Arc::clone()仅增加引用计数(耗时约 1 纳秒),而不拷贝数据let flag = stop_flag.clone();
将数据移动到新生成的线程线程需要持有其自身的一份独立副本let ctx = ctx.clone(); thread::spawn(move || { ... })
从 &self 字段中提取数据无法从借用中移动数据在需要返回拥有所有权的 String 时使用 self.name.clone()
包装在 Option 中的小型 Copy 类型使用 .copied() 比 .clone() 语义更清晰opt.get(0).copied() 可将 Option<&u32> 转为 Option<u32>

真实示例:用于线程共享的 Arc::clone

#![allow(unused)]
fn main() {
// 示例:workload.rs —— Arc::clone 极其轻量(仅增加引用计数)
let stop_flag = Arc::new(AtomicBool::new(false));
let stop_flag_clone = stop_flag.clone();   // 耗时约 1 纳秒,无数据拷贝
let ctx_clone = ctx.clone();               // 为移动至线程而克隆上下文

let sensor_handle = thread::spawn(move || {
    // ... 在此处使用 stop_flag_clone 和 ctx_clone
});
}

检查清单:我是否需要克隆?

  1. 我是否能接受使用 &str / &T 来替代 String / T? → 应尽量借用而不是克隆。
  2. 能否重构代码以避免共用所有权? → 尽量通过引用传递或在代码块中限定作用域。
  3. 这是 Arc::clone() 吗? → 这是可以的,其算法复杂度为常数级 O(1)。
  4. 我需要将数据移动到线程或闭包中吗? → 这种情况下克隆是必需的。
  5. 我是不是在性能热点或循环内部进行克隆? → 请进行性能评估,并考虑改为借用或使用写时复制(Cow<T>)。

Cow<'a, T>: 写时复制 (Clone-on-Write) —— 尽可能借用,必要时克隆

Cow (Clone-on-Write) 是一个枚举类型,它可以持有一个借用的引用,或者持有一个拥有的值。它是 Rust 中的内置类型,代表了“尽可能避免分配内存,但如果需要修改,则分配内存”的策略。C++ 中没有直接的等价实现 —— 最接近的做法是根据情况有时返回 const std::string&,有时返回 std::string 的函数。

为什么需要 Cow

#![allow(unused)]
fn main() {
// 如果不使用 Cow —— 你必须二选一:总是借用,或者总是克隆
fn normalize(s: &str) -> String {          // 总是会进行内存分配!
    if s.contains(' ') {
        s.replace(' ', "_")               // 生成新的 String(需要分配内存)
    } else {
        s.to_string()                     // 做了不必要的分配!
    }
}

// 使用 Cow —— 仅在修改时才进行分配,其余情况仅借用
use std::borrow::Cow;

fn normalize(s: &str) -> Cow<'_, str> {
    if s.contains(' ') {
        Cow::Owned(s.replace(' ', "_"))    // 发生了修改,必须分配内存
    } else {
        Cow::Borrowed(s)                   // 零分配,直接透传引用
    }
}
}

Cow 是如何运作的

use std::borrow::Cow;

// Cow<'a, str> 核心逻辑等效于如下定义:
// enum Cow<'a, str> {
//     Borrowed(&'a str),     // 零拷贝的引用
//     Owned(String),          // 属于该变量的 String (位于堆上)
// }

fn greet(name: &str) -> Cow<'_, str> {
    if name.is_empty() {
        Cow::Borrowed("陌生人")             // 静态字符串 —— 零分配
    } else if name.starts_with(' ') {
        Cow::Owned(name.trim().to_string()) // 发生了修剪 —— 需要分配内存
    } else {
        Cow::Borrowed(name)                 // 直接透传 —— 零分配
    }
}

fn main() {
    let g1 = greet("Alice");     // Cow::Borrowed("Alice")
    let g2 = greet("");          // Cow::Borrowed("陌生人")
    let g3 = greet(" Bob ");     // Cow::Owned("Bob")
    
    // Cow<str> 实现了 Deref<Target = str>,因此你可以像对待 &str 那样使用它:
    println!("你好, {g1}!");    // 正常工作 —— Cow 自动解引用为 &str
    println!("你好, {g2}!");
    println!("你好, {g3}!");
}

现实应用场景:配置信息规范化

use std::borrow::Cow;

/// 规范化 SKU 名称:修整两端空格并转为小写。
/// 如果已经是规范化后的,则返回 Cow::Borrowed (零分配)。
fn normalize_sku(sku: &str) -> Cow<'_, str> {
    let trimmed = sku.trim();
    if trimmed == sku && sku.chars().all(|c| c.is_lowercase() || !c.is_alphabetic()) {
        Cow::Borrowed(sku)   // 已是规范化状态 —— 零分配
    } else {
        Cow::Owned(trimmed.to_lowercase())  // 需要修改 —— 重新分配内存
    }
}

fn main() {
    let s1 = normalize_sku("server-x1");   // Borrowed —— 零分配
    let s2 = normalize_sku("  Server-X1 "); // Owned —— 必须分配
    println!("{s1}, {s2}"); // "server-x1, server-x1"
}

何时使用 Cow

场景是否使用 Cow?
大部分情况下函数都会原样返回输入值✅ 是 —— 避免不必要的克隆
解析/规范化字符串(修剪、小写、替换等)✅ 是 —— 通常输入已经是目标状态
每一条代码路径都会进行修改并导致分配❌ 否 —— 直接返回 String 即可
简单的透传(从未发生修改)❌ 否 —— 直接返回 &str 即可
需要将数据长期存储在结构体中❌ 否 —— 直接使用拥有所有权的 String

C++ 开发者的类比:Cow<str> 就像是一个返回 std::variant<std::string_view, std::string> 的函数 —— 只不过它具备自动解解引用机制,访问其值时无需编写任何繁琐的样板代码。


Weak<T>: 打破引用循环 —— Rust 中的 weak_ptr

Weak<T> 是 Rust 中与 C++ 的 std::weak_ptr<T> 等效的类型。它持有一个指向 Rc<T> 或 Arc<T> 值的非拥有引用。即便 Weak 引用仍然存在,其指向的值也可以被销毁 —— 如果目标值已不存在,调用 upgrade() 将返回 None。

为什么需要 Weak

如果两个值相互指向对方,Rc<T> 和 Arc<T> 就会产生引用循环 —— 导致两者的引用计数永远无法归零,从而无法被释放(即产生内存泄漏)。Weak 可以有效地打破这种循环:

use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: String,
    parent: RefCell<Weak<Node>>,      // Weak —— 不会阻止父节点被释放
    children: RefCell<Vec<Rc<Node>>>,  // Strong —— 父节点拥有子节点的所有权
}

impl Node {
    fn new(value: &str) -> Rc<Node> {
        Rc::new(Node {
            value: value.to_string(),
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(Vec::new()),
        })
    }

    fn add_child(parent: &Rc<Node>, child: &Rc<Node>) {
        // 子节点获得指向父节点的弱引用(无循环引用)
        *child.parent.borrow_mut() = Rc::downgrade(parent);
        // 父节点获得指向子节点的强引用
        parent.children.borrow_mut().push(Rc::clone(child));
    }
}

fn main() {
    let root = Node::new("根节点");
    let child = Node::new("子节点");
    Node::add_child(&root, &child);

    // 通过 upgrade() 从子节点访问父节点
    if let Some(parent) = child.parent.borrow().upgrade() {
        println!("子节点的父节点: {}", parent.value); // "根节点"
    }
    
    println!("根节点强引用计数: {}", Rc::strong_count(&root));  // 1
    println!("根节点弱引用计数: {}", Rc::weak_count(&root));      // 1
}

C++ 对比

// C++ — 使用 weak_ptr 打破 shared_ptr 循环引用
struct Node {
    std::string value;
    std::weak_ptr<Node> parent;                  // Weak — 无所有权
    std::vector<std::shared_ptr<Node>> children;  // Strong — 拥有子节点
    
    static auto create(const std::string& v) {
        return std::make_shared<Node>(Node{v, {}, {}});
    }
};

auto root = Node::create("root");
auto child = Node::create("child");
child->parent = root;          // weak_ptr 赋值
root->children.push_back(child);

if (auto p = child->parent.lock()) {   // lock() → 得到 shared_ptr 或空值
    std::cout << "Parent: " << p->value << std::endl;
}
C++Rust说明
shared_ptr<T>Rc<T> (单线程) / Arc<T> (多线程)语义相同
weak_ptr<T>通过 Rc::downgrade() / Arc::downgrade() 获得的 Weak<T>语义相同
weak_ptr::lock() → shared_ptr 或空Weak::upgrade() → Option<Rc<T>>如果已释放则返回 None
shared_ptr::use_count()Rc::strong_count()含义相同

何时使用 Weak

场景模式
父 ↔ 子 树形关系父节点持有 Rc<Child>,子节点持有 Weak<Parent>
观察者模式 / 事件监听器事件源持有 Weak<Observer>,观察者持有 Rc<Source>
不阻碍释放的缓存HashMap<Key, Weak<Value>> —— 条目会自然过期
打破图结构中的循环交叉链接使用 Weak,树边使用 Rc/Arc

提示:在编写新代码时,相较于 Rc/Weak,更推荐使用 Arena 模式(参见案例研究 2)来构建树形结构。Vec<T> + 索引的方式更简单、更快速,且具备零引用计数开销。仅当你确实需要具有动态生命周期的共享所有权时,才使用 Rc/Weak。


Copy 对比 Clone,PartialEq 对比 Eq —— 应在何时派生哪些 Trait

  • Copy ≈ C++ 的平凡可复制 (Trivially Copyable,无自定义拷贝构造函数/析构函数)。对于如 int、enum 以及简单的 POD 结构体,编译器会自动生成按位拷贝的 memcpy。在 Rust 中,Copy 的理念也是如此:赋值操作 let b = a; 会执行隐式的按位拷贝,且两个变量在此后依然有效。
  • Clone ≈ C++ 的拷贝构造函数 / operator= 深拷贝。当一个 C++ 类拥有自定义拷贝构造函数(例如深拷贝一个 std::vector 成员)时,Rust 中的等效做法是实现 Clone。你必须显式调用 .clone() —— Rust 绝不会将开销巨大的拷贝操作掩盖在 = 赋值符号之后。
  • 关键区别:在 C++ 中,平凡拷贝和深拷贝都通过相同的 = 语法隐式发生。而 Rust 迫使你进行选择:Copy 类型会默默拷贝(开销极低),非 Copy 类型默认会执行 移动 (Move) 语义,你必须通过 .clone() 显式选择执行开销巨大的数据副本。
  • 类似地,C++ 的 operator== 并不区分 a == a 总是成立的类型(如整数)和不成立的类型(如带有 NaN 的浮点数)。Rust 在 PartialEq 与 Eq 中对这种区别进行了编码。

Copy 对比 Clone

CopyClone
工作原理按位 memcpy (隐式发生)自定义逻辑 (显式调用 .clone())
发生时机赋值时:let b = a;仅当你显式调用 .clone() 时
拷贝/克隆后a 和 b 均保持有效a 和 b 均保持有效
不具备两者时let b = a; 会移动 a (a 变无效)let b = a; 会移动 a (a 变无效)
适用范围不持有堆数据的类型任何类型
C++ 类比平凡可复制 / POD 类型 (无自定义拷贝构造)自定义拷贝构造函数 (深拷贝)

真实示例:Copy —— 简单枚举

#![allow(unused)]
fn main() {
// 摘自 fan_diag/src/sensor.rs —— 均为单元变体,占用 1 字节
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum FanStatus {
    #[default]
    Normal,
    Low,
    High,
    Missing,
    Failed,
    Unknown,
}

let status = FanStatus::Normal;
let copy = status;   // 隐式拷贝 —— status 依然有效
println!("{:?} {:?}", status, copy);  // 两者均可正常使用
}

真实示例:Copy —— 带有整数负载的枚举

#![allow(unused)]
fn main() {
// 示例:healthcheck.rs —— u32 负载支持 Copy,因此整个枚举也支持
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HealthcheckStatus {
    Pass,
    ProgramError(u32),
    DmesgError(u32),
    RasError(u32),
    OtherError(u32),
    Unknown,
}
}

真实示例:仅限 Clone —— 持有堆数据的结构体

#![allow(unused)]
fn main() {
// 示例:components.rs —— String 类型导致其无法支持 Copy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FruData {
    pub technology: DeviceTechnology,
    pub physical_location: String,      // ← String 类型位于堆上,无法进行 Copy
    pub expected: bool,
    pub removable: bool,
}
// let a = fru_data;   → 会发生移动 (move),原变量 fru_data 变得无效
// let a = fru_data.clone();  → 克隆操作 (fru_data 依然有效,同时发生了新的堆内存分配)
}

判定准则:是否可以支持 Copy?

该类型是否包含 String、Vec、Box、HashMap、
Rc、Arc 或任何其他持有堆内存所有权的类型?
    是 → 仅限 Clone (无法支持 Copy)
    否 → 你可以派生 Copy Trait (如果类型占用空间较小,建议派生)

PartialEq 对比 Eq

PartialEqEq
提供的功能== 和 != 运算符标记 Trait:“相等关系满足自反性”
自反性?(a == a)不保证保证
重要性何在f32::NAN != f32::NANHashMap 的键 必须 实现 Eq
何时派生几乎所有类型当类型不包含 f32/f64 字段时
C++ 类比operator==无直接对应项 (C++ 不进行此类检查)

真实示例:Eq —— 用作 HashMap 的键

#![allow(unused)]
fn main() {
// 摘自 hms_trap/src/cpu_handler.rs —— Hash 要求实现 Eq
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CpuFaultType {
    InvalidFaultType,
    CpuCperFatalErr,
    CpuLpddr5UceErr,
    CpuC2CUceFatalErr,
    // ...
}
// 用法:HashMap<CpuFaultType, FaultHandler>
// HashMap 的键必须同时实现 Eq + Hash —— 仅实现 PartialEq 将无法通过编译
}

真实示例:无法实现 Eq —— 类型包含 f32

#![allow(unused)]
fn main() {
// 示例:types.rs —— f32 阻碍了 Eq 的实现
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TemperatureSensors {
    pub warning_threshold: Option<f32>,   // ← f32 存在 NaN ≠ NaN 的情况
    pub critical_threshold: Option<f32>,  // ← 无法派生 Eq
    pub sensor_names: Vec<String>,
}
// 该类型无法用作 HashMap 的键。无法派生 Eq。
// 原因:f32::NAN == f32::NAN 的结果为 false,违反了自反性。
}

PartialOrd 对比 Ord

PartialOrdOrd
提供的功能<, >, <=, >= 运算符.sort(), BTreeMap 的键
全序关系?否(某些值可能无法比较)是(任意两个值均可比较)
f32/f64?仅支持 PartialOrd (NaN 会破坏顺序)无法派生 Ord

真实示例:Ord —— 严重程度排序

#![allow(unused)]
fn main() {
// 摘自 hms_trap/src/fault.rs —— 变体的顺序决定了严重程度
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FaultSeverity {
    Info,      // 最低 (判别值为 0)
    Warning,   //      (判别值为 1)
    Error,     //      (判别值为 2)
    Critical,  // 最高 (判别值为 3)
}
// FaultSeverity::Info < FaultSeverity::Critical → true
// 使得如下逻辑成为可能:if severity >= FaultSeverity::Error { escalate(); }
}

真实示例:Ord —— 用于比较的诊断级别

#![allow(unused)]
fn main() {
// 示例:orchestration.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum GpuDiagLevel {
    #[default]
    Quick,     // 最低
    Standard,
    Extended,
    Full,      // 最高
}
// 使得如下逻辑成为可能:if requested_level >= GpuDiagLevel::Extended { run_extended_tests(); }
}

派生决策树

                        你的新类型
                             │
                   是否包含 String/Vec/Box?
                       /              \
                     是                否
                     │                  │
               仅限 Clone          Clone + Copy
                     │                  │
               是否包含 f32/f64?    是否包含 f32/f64?
                 /          \         /          \
               是           否       是           否
               │             │      │             │
         仅派生             派生    仅派生         派生
         PartialEq       PartialEq  PartialEq  PartialEq
         只有其一         + Eq       只有其一    + Eq
                           │                      │
                     是否需要排序?           是否需要排序?
                       /       \               /       \
                     是        否             是        否
                     │          │              │          │
               PartialOrd      完成        PartialOrd    完成
               + Ord                     + Ord
                     │                        │
               是否需要用作             是否需要用作
               Map 的键?                 Map 的键?
                   │                        │
                 + Hash                   + Hash

快速参考:生产环境 Rust 代码中的常见派生组合

类型类别典型派生组合示例
简单的状态枚举Copy, Clone, PartialEq, Eq, DefaultFanStatus
用作 HashMap 键的枚举Copy, Clone, PartialEq, Eq, HashCpuFaultType, SelComponent
可排序的严重程度枚举Copy, Clone, PartialEq, Eq, PartialOrd, OrdFaultSeverity, GpuDiagLevel
持有 String 的数据结构体Clone, Debug, Serialize, DeserializeFruData, OverallSummary
可序列化的配置信息Clone, Debug, Default, Serialize, DeserializeDiagConfig