所有权:编译器为什么拒绝我

什么时候就该 clone

它在解决什么

这一章从头到尾讲的都是「编译器为什么拒绝你」,给出的解法也基本都是 「改写法、别复制」。连起来读很容易得到一个印象:.clone() 是失败的标志。

这个印象会让人付出真实代价 —— 对着一个借用冲突改上三个小时, 而正确答案本来就是加一个 .clone()。

所以这一篇反过来讲:clone 到底多贵、什么时候它就是对的答案、 以及什么时候它确实是在替一个设计问题买单。

先搞清楚 clone 到底在做什么

.clone() 不是一个操作,是三类完全不同的操作共用一个名字。

第一类:真的深拷贝。 Vec<String> 的 clone 会把 Vec 自己、 以及里面每个 String 拥有的那块堆内存,逐层重新分配一份。 改克隆出来的那份完全不会碰到原件 —— 这既是它的价值,也是它的价格。

第二类:几乎免费。 Rc::clone 只是把引用计数加一,一个字节的数据都没复制。

这两条并排看最清楚:同一份数据,两种 clone 打印出来的 长度一模一样(都是 10),区别全在那个计数上 —— Rc::clone 之后是 3 (三个名字共享同一份),真 clone 之后还是 1(另外两份各自独立)。

⇒ 所以「clone 很贵」这句话本身是错的。看到 .clone() 先问一句「clone 的是什么类型」。

第三类:根本不需要。 在 Copy 类型上写 .clone() 什么问题都没解决 —— let c = a 对 i32 本来就不会让 a 失效。这一类纯粹是噪音, 通常来自「被借用检查器打怕了,见到赋值就先 clone 一下」。

三种情况下 clone 就是对的答案

一、值必须活过那次修改。

这段代码里,first 要在 names.clear() 之后还能用。 去掉 clone 换成借用当场 E0502。

这里没有更聪明的写法。你要的就是一个能活过那次修改的值, 而「活过」意味着它不能是借用。clone 是这个场景的正确答案,不是妥协。

⚠️ 但先确认你真的需要它活那么久。这一条是我写这篇时 被闸门当场改出来的:我以为「借用 + push」必然冲突,实测完全合法 ——

names.push(format!("{}-copy", &names[0]));   // 这是过得去的

因为 push 的可变借用属于两阶段借用(two-phase borrow): 它在求值参数期间只是「预留」,直到真正调用那一刻才激活。 所以在参数里读一下完全没问题。

⇒ 判据:借用和修改的先后顺序,比「有没有同时出现」更重要。 把借用留到 push 之后才用,那才是真冲突。

二、不想让类型背上生命周期。

把 &str 存进结构体要带 <'a>(否则 E0106), 而那个标注会往上传染到每个 impl、每个装它的类型、每个返回它的函数。

一次 to_string() 换掉整条传染链。 在绝大多数应用代码里这都是划算的 —— 你省下的那次分配, 换来的是整个类型体系少一个维度。

📌 这条就是生命周期那篇里那句 「能存 String 就别存 &str」的具体理由。

三、在成为瓶颈之前。

这条最没技术含量,但省的时间最多:先让它编译过。

Rust 和别的语言有个不对称的地方 —— 在 Java 里写得难看的代码照样能跑, 你可以边跑边改;在 Rust 里编译不过就什么都没有。所以「先 clone 通过、 跑起来之后再看要不要优化」在这门语言里比在别处更值得。

而「要不要优化」是有判据的:测过再说。这一篇一个耗时数字都没给, 正是因为那种数字换台机器就可能给出相反的结论。

两个「这个 clone 在掩盖问题」的信号

信号一:它加在 Copy 类型上。 见上面第三类。这说明写它的人没在判断,只是在条件反射。

信号二:被调方根本不需要所有权。

这段代码里 shout 的参数是 String, 于是调用方被迫 clone 一份给它。但 shout 内部只是读一下就生成新串 —— 它根本不需要所有权。把参数改成 &str,调用方那个 clone 就消失了。

⇒ 判据:先看被调方要所有权干什么。 答得上来(要存起来、要改、要转手出去), 那 clone 是应该的;答不上来,那个 clone 就是在替一个签名错误买单。

🚨 这条特别值得留意,因为它在输出上完全看不出来。 那条示例是本板块少数几个写了「没有对照项」的 —— 正确写法和错误写法 跑出来一模一样:同样编译通过、同样打印 ANN / ann。两者唯一的差别是 多了一次堆分配。

⇒ 所以没有人会因为「跑出来不对」而发现过度 clone。它只能靠读代码时 主动问那一句。

这一章到这里就完整了

回头看,五篇讲的是同一件事的五个层面:

篇 它回答的问题
移动 赋值之后原来那个名字为什么作废
借用 怎么不转移所有权地用一下
生命周期 借用能活多久,谁来说清楚
切片 借一段而不是整个,以及 UTF-8 的代价
本篇 什么时候干脆别借,复制一份

下一步

所有权解决的是「值归谁、活多久」。接下来的一章换一个方向: 怎么把「有哪几种情况」写进类型里,让编译器帮你数 —— 从《struct 与 enum:把状态说清楚》开始。

全部篇目见 Rust 教程首页。

本篇示例

下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。

clone 出来的是**独立的一份**

fn main() {
    let a = vec![String::from("x"), String::from("y")];
    let mut b = a.clone();
    b[0].push('!');
    println!("{a:?} {b:?}");
}

编译通过 · 输出 "[\"x\", \"y\"] [\"x!\", \"y\"]\n"

对照:把 `a.clone()` 改成 `a`(不是复制,是搬走)
fn main() {
    let a = vec![String::from("x"), String::from("y")];
    let mut b = a;
    b[0].push('!');
    println!("{a:?} {b:?}");
}

编译不过:error[E0382]

改 `b` 完全没碰到 `a` —— 而且是**逐层深入**的:`Vec` 自己、里面每个 `String` 拥有的堆内存,全都重新分配了一份。这就是 clone 的价格所在,也是它和 `Rc::clone` 的根本区别。

`Rc::clone` 几乎免费:它只加一个计数

use std::rc::Rc;

fn main() {
    let s = Rc::new(String::from("hello"));
    let a = Rc::clone(&s);
    let b = Rc::clone(&s);
    println!("{} {}", Rc::strong_count(&s), a.len() + b.len());
}

编译通过 · 输出 "3 10\n"

对照:改成 `(*s).clone()`(真的复制那个 String)
use std::rc::Rc;

fn main() {
    let s = Rc::new(String::from("hello"));
    let a = (*s).clone();
    let b = (*s).clone();
    println!("{} {}", Rc::strong_count(&s), a.len() + b.len());
}

编译通过,输出:"1 10\n"

两边打印的长度一模一样(都是 10),**区别全在那个计数上**:`Rc::clone` 之后是 3,说明三个名字共享同一份数据;真 clone 之后还是 1,说明另外两份是各自独立的副本。⇒ 看到 `.clone()` 先问一句「clone 的是什么类型」—— `Rc`/`Arc` 的 clone 和 `String`/`Vec` 的 clone 是两件完全不同的事。

正当用法一:值必须活过那次修改

fn main() {
    let mut names = vec![String::from("ann"), String::from("bob")];
    let first = names[0].clone();
    names.clear();
    println!("{first}");
}

编译通过 · 输出 "ann\n"

对照:把 `.clone()` 去掉,改成借用 `&names[0]`
fn main() {
    let mut names = vec![String::from("ann"), String::from("bob")];
    let first = &names[0];
    names.clear();
    println!("{first}");
}

编译不过:error[E0502]

对照项报 `E0502`:`first` 借着 `names`,而 `clear` 要可变借用,偏偏 `first` 在那之后还要用。这里**没有更聪明的写法** —— 你确实需要一个能活过那次修改的值。clone 是这个场景的正确答案,不是妥协。

🚨 但「改容器」本身不一定需要 clone

fn main() {
    let mut names = vec![String::from("ann"), String::from("bob")];
    names.push(format!("{}-copy", &names[0]));
    println!("{names:?}");
}

编译通过 · 输出 "[\"ann\", \"bob\", \"ann-copy\"]\n"

对照:把借用留到 `push` **之后**再用
fn main() {
    let mut names = vec![String::from("ann"), String::from("bob")];
    let first = &names[0];
    names.push(String::from("carl"));
    println!("{first}");
}

编译不过:error[E0502]

这条是**闸门帮我改出来的**:初稿把上一条写成「借用 + push 会报 E0502」,实测两边跑出完全一样的结果 —— 因为 `push` 的可变借用属于**两阶段借用**(two-phase borrow):它在求值参数期间只是「预留」,直到真正调用那一刻才激活。所以在参数里读一下 `&names[0]` 完全合法。⇒ 判据:**借用和修改的先后顺序比「有没有同时出现」更重要**。对照项把借用留到 `push` 之后才用,那才是真冲突(`E0502`)。

正当用法二:让类型不背上生命周期

struct Config {
    name: String,
}

fn make(name: &str) -> Config {
    Config { name: name.to_string() }
}

fn main() {
    let c = make("prod");
    println!("{}", c.name);
}

编译通过 · 输出 "prod\n"

对照:把字段改成 `&str`(省掉那次分配)
struct Config {
    name: &str,
}

fn make(name: &str) -> Config {
    Config { name }
}

fn main() {
    let c = make("prod");
    println!("{}", c.name);
}

编译不过:error[E0106]

对照项报 `E0106`:结构体存引用就必须带生命周期标注,而那个 `<'a>` 会往上传染到每个 `impl`、每个装它的类型。**一次 `to_string()` 换掉整条传染链**,在绝大多数应用代码里都是划算的。

信号一:在 `Copy` 类型上写 `.clone()`

fn main() {
    let a = 5;
    let b = a.clone();
    let c = a;
    println!("{a} {b} {c}");
}

编译通过 · 输出 "5 5 5\n"

对照:把 `i32` 换成 `String`(其余一字不改)
fn main() {
    let a = String::from("5");
    let b = a.clone();
    let c = a;
    println!("{a} {b} {c}");
}

编译不过:error[E0382]

`i32` 是 `Copy`,`let c = a` 本来就不会让 `a` 失效 —— 所以那个 `.clone()` 一点作用都没有,只是噪音。对照项把类型换成 `String`,同样的三行当场 `E0382`。⇒ 看到 `.clone()` 时先确认它**真的在解决一个问题**。

🚨 信号二:clone 掉的东西调用方根本不需要

fn shout(s: String) -> String {
    s.to_uppercase()
}

fn main() {
    let name = String::from("ann");
    println!("{}", shout(name.clone()));
    println!("{name}");
}

编译通过 · 输出 "ANN\nann\n"

这条没有对照项:正确写法(把参数改成 `&str`、调用处不 clone)**跑出来一模一样**:同样编译通过、同样打印 ANN / ann。两者唯一的差别是多了一次堆分配,而本板块一个性能数字都不断言(理由见 rust-examples.ts 顶部 ⑤:同一份代码在本机和目标机上能给出相反的结论)。⭐ 这正是这条示例的教学点:**过度 clone 在输出上完全看不出来**,所以没人会因为「跑出来不对」而发现它。

`shout` 要走所有权,于是调用方被迫 `clone` 一份给它。但 `shout` 内部只是读一下就生成新串 —— 它**根本不需要**所有权。把参数改成 `&str` 之后调用方就不用 clone 了。⇒ 判据:**先看被调方要所有权干什么**。答不上来,那个 clone 就是在替一个签名错误买单。