首页
/ Rustlings Move Semantics 实战:用五道递进练习吃透 Rust 的所有权、移动与借用

Rustlings Move Semantics 实战:用五道递进练习吃透 Rust 的所有权、移动与借用

2026-09-04 09:36:10作者:范靓好Udolf

Rust 的所有权系统(Ownership)是语言最核心也最劝退新人的概念,rustlings 仓库中的 exercises/06_move_semantics 章节正是围绕这一主题设计的五道递进练习(move_semantics1~5)。本文以该章节的 README 与五道练习文件为主体,结合 solutions/06_move_semantics 中的官方解法、rustlings-macros/info.toml 中每道练习内置的 hint 文本,以及 rustlings 运行器源码(src/exercise.rs),完整拆解每道练习的报错原因、修改思路与背后原理,帮助读者真正掌握“移动语义 + 借用规则”的决策方法。

一、章节定位:README 说明了什么

exercises/06_move_semantics/README.md 篇幅很精炼,但给出了两条关键信息:

  1. 本章练习改编自 pnkfelix 的 Rust Tutorial(ICFP 2014 教程),说明这些题目是经过多年教学验证的经典题型;
  2. 本章配套 Rust 官方文档(The Rust Book)的两个章节:Ownership(所有权)References and borrowing(引用与借用)。仓库的 exercises/README.md 中也确认了这一映射:move_semantics 对应书中 §4.1–4.2。

因此本章的学习路径非常明确:先把所有权模型讲清楚——每个值有且只有一个所有者;所有者离开作用域时值被释放;把值传给函数或函数返回时发生“移动”(move),原绑定不可再用;再看借用如何避免不必要的移动——不可变引用 &T 可以多个并存,可变引用 &mut T 同一时刻只能有一个,且二者不能与前者并存。

本章共五道练习,难度层层递进:

练习 文件 核心考点
move_semantics1 move_semantics1.rs 可变绑定与 push 的编译错误
move_semantics2 move_semantics2.rs “borrow of moved value” 与 clone
move_semantics3 move_semantics3.rs 直接声明参数为 mut
move_semantics4 move_semantics4.rs 可变引用的排他使用与借用检查
move_semantics5 move_semantics5.rs 按引用传参与按值传参(取所有权)的选择

二、rustlings 如何驱动这些练习

理解运行方式有助于把报错信息对号入座。从 dev/Cargo.toml 可以看到,每道练习及其解法都被注册为独立的 bin target:

{ name = "move_semantics1", path = "../exercises/06_move_semantics/move_semantics1.rs" },
{ name = "move_semantics1_sol", path = "../solutions/06_move_semantics/move_semantics1.rs" },
# ……move_semantics2~5 同理

其中四个练习(1~4)内部带 #[cfg(test)] 测试,第 5 题则是一个可执行程序(main 直接运行)。rustlings 的 src/exercise.rsExercise 结构体携带了 namedirpathteststrict_clippyhint 等字段,与 rustlings-macros/info.toml 中的 [[exercises]] 配置一一对应——hint 文本正是 watch 模式下按 h 显示的那段提示。

实际操作上:克隆仓库并初始化后运行 rustlings 进入 watch 模式,修改 exercises/ 下当前练习文件即会自动重新编译运行;按 l 可打开练习列表,用 c 跳到任意练习、r 重置某道练习的文件与状态。更多键位说明见 website/content/usage/index.md。卡住时按 h 查看官方 hint 是本章官方推荐的用法(例如 move_semantics4 的 hint 直接提示读者去分析每个可变引用的作用域区间)。

三、move_semantics1:从“不能可变借用”开始

题目代码(exercises/06_move_semantics/move_semantics1.rs):

// TODO: Fix the compiler error in this function.
fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
    let vec = vec;

    vec.push(88);

    vec
}

fn main() {
    // You can optionally experiment here.
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn move_semantics1() {
        let vec0 = vec![22, 44, 66];
        let vec1 = fill_vec(vec0);
        assert_eq!(vec1, vec![22, 44, 66, 88]);
    }
}

报错发生在 vec.push(88)cannot borrow vec as mutable, as it is not declared as mutable。这里 vec 是按值传入的 Vec<i32>,函数体内 let vec = vec; 又新建了一个不可变绑定,push 需要 &mut self,因此编译失败。

官方解法(solutions/06_move_semantics/move_semantics1.rs)只加了一个关键字:

fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
    let mut vec = vec;
    //  ^^^ added

    vec.push(88);

    vec
}

info.toml 中这道题的 hint 还给了一个进阶思考方向:“试着在调用 fill_vec() 之后访问 vec0,看看会发生什么”——因为 fill_vec(vec0)vec0 的所有权移走了,调用后再使用 vec0 会触发下一题的报错。

四、move_semantics2:“借用已移动的值”与 clone

题目要求让 vec0vec1 同时可用(exercises/06_move_semantics/move_semantics2.rs):

fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
    let mut vec = vec;

    vec.push(88);

    vec
}

#[cfg(test)]
mod tests {
    use super::*;

    // TODO: Make both vectors `vec0` and `vec1` accessible at the same time to
    // fix the compiler error in the test.
    #[test]
    fn move_semantics2() {
        let vec0 = vec![22, 44, 66];

        let vec1 = fill_vec(vec0);

        assert_eq!(vec0, [22, 44, 66]);   // 错误:borrow of moved value: `vec0`
        assert_eq!(vec1, [22, 44, 66, 88]);
    }
}

fill_vec(vec0) 按值传参,vec0 的所有权随调用移入函数,之后的 assert_eq!(vec0, ...) 是对已移动值的借用,编译器直接拒绝。官方 hint(rustlings-macros/info.toml)原样解释了这一点:

In Rust, when an argument is passed to a function and it's not explicitly returned, you can't use the original variable anymore. We call this "moving" a variable. … You could make another, separate version of the data that's in vec0 and pass it to fill_vec instead. This is called cloning in Rust.

官方解法(solutions/06_move_semantics/move_semantics2.rs):

// Cloning `vec0` so that the clone is moved into `fill_vec`, not `vec0` itself.
let vec1 = fill_vec(vec0.clone());

Vec<i32> 没有实现 Copy,因此按值传递就是移动;clone() 产生一份完整拷贝,拷贝被移走而 vec0 本体安然无恙。这引出所有权的第一条决策原则:只有“不再需要原值”时才让它被移动;需要原值则考虑 clone(有堆内存复制成本)或借用(无成本但受借用规则约束)

五、move_semantics3:让参数本身可变,少写一行

题目(exercises/06_move_semantics/move_semantics3.rs)刻意删掉了第 1 题解法中的 let mut vec = vec;,并要求不新增任何一行来修复错误:

// TODO: Fix the compiler error in the function without adding any new line.
fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
    vec.push(88);

    vec
}

此时 push 仍然失败,因为函数参数 vec 默认是不可变绑定。hint 的提示是:“与其把 let mut vec = vec; 加回来,不如在某个位置加上 mut,把一个既有绑定从不可变改成可变绑定”。解法(solutions/06_move_semantics/move_semantics3.rs):

fn fill_vec(mut vec: Vec<i32>) -> Vec<i32> {
    //      ^^^ added
    vec.push(88);

    vec
}

在参数声明处直接写 mut vec 是更地道的 Rust 写法:参数本身就是一个绑定,直接声明为可变即可,省去了“用同名不可变绑定遮蔽自己”的冗余代码。这道题顺带强化了 Rust 的通用规则——绑定默认不可变,mut 必须显式声明,这与很多动态/垃圾回收语言“变量默认可变”的直觉正好相反。

六、move_semantics4:可变引用同一时刻只能有一个

前四题的主角还是所有权,第 4 题(exercises/06_move_semantics/move_semantics4.rs)进入借用规则的正题,且约束苛刻:只能交换行序,不能增删或修改任何一行

#[test]
fn move_semantics4() {
    let mut x = Vec::new();
    let y = &mut x;
    let z = &mut x;      // 错误:cannot borrow `x` as mutable more than once
    y.push(42);
    z.push(13);
    assert_eq!(x, [42, 13]);
}

Rust 的借用检查器规定:对同一变量的可变引用,同一时刻只能存在一个(防止数据竞争与别名写冲突)。原代码中 yz 两个 &mut x 在创建时并存,即使后续是先后使用,也在编译期被判非法。

解法利用的是 NLL(Non-Lexical Lifetimes,非词法生命周期) 对借用“结束点”的精确分析——只要前一个可变引用最后一次被使用之后才创建下一个,两者生命周期就不再重叠。官方解法(solutions/06_move_semantics/move_semantics4.rs):

let mut x = Vec::new();
let y = &mut x;
// `y` used here.
y.push(42);
// The mutable reference `y` is not used anymore,
// therefore a new reference can be created.
let z = &mut x;
z.push(13);
assert_eq!(x, [42, 13]);

注意 y 最后一次使用在 y.push(42)let z = &mut x; 发生在其后,借用区间自然错开。info.toml 中该题的 hint 也点题了“Carefully reason about the range in which each mutable reference is in scope”(rustlings-macros/info.toml)。这道题的实际意义在于:把“借用规则是词法作用域”这一常见误解纠正为“借用生命周期以最后一次使用为准”,这是阅读复杂 Rust 代码时判断借用冲突的关键心智模型。

七、move_semantics5:借用来读、按值来改——按语义选择传参方式

第 5 题是本章的综合题(exercises/06_move_semantics/move_semantics5.rs),约束是只能增删 & 字符,其余不许改。题目中两个函数的注释直接标出了语义意图:

#![allow(clippy::ptr_arg)]

// Shouldn't take ownership
fn get_char(data: String) -> char {          // 注释说“不该”取所有权,签名却按值
    data.chars().last().unwrap()
}

// Should take ownership
fn string_uppercase(mut data: &String) {     // 注释说“应该”取所有权,签名却是引用
    data = data.to_uppercase();

    println!("{data}");
}

fn main() {
    let data = "Rust is great!".to_string();

    get_char(data);
    string_uppercase(&data);
}

问题在于两处“语义与签名错位”:

  • get_char 只读最后一个字符,却按值拿走 data,导致 data 被移动;
  • string_uppercase 内部执行 data = data.to_uppercase();替换绑定,必须持有所有权才能完成,签名却只是 &String——对不可变引用根本赋不了值(即使改成 &mut 也不能把 String 赋回引用指向的位置,因为引用指向的是 String 本体而非绑定本身)。

官方解法(solutions/06_move_semantics/move_semantics5.rs)把 & 放到正确的一边:

// Borrows instead of taking ownership.
// It is recommended to use `&str` instead of `&String` here. But this is
// enough for now because we didn't handle strings yet.
fn get_char(data: &String) -> char {
    data.chars().last().unwrap()
}

// Takes ownership instead of borrowing.
fn string_uppercase(mut data: String) {
    data = data.to_uppercase();

    println!("{data}");
}

fn main() {
    let data = "Rust is great!".to_string();

    get_char(&data);
    string_uppercase(data);
}

两个调用顺序也体现了所有权模型:get_char(&data) 只是借用,data 之后仍然可用;string_uppercase(data) 把它移走作为最后一步,顺序颠倒会编译失败。解法注释还顺带埋了一个进阶知识点:更惯用的写法是 &str(字符串切片引用)而非 &String——不过本章还没涉及 strings 章节,暂不展开。这道题给出的实用结论是:判断一个参数该借用还是按值传递,看函数是否需要“改走”这个值本身——只读取就借用(零成本),要重新绑定/转移所有权就按值传入。

八、小结与验证路径

把五道练习串起来,正好构成所有权学习的完整阶梯:

  1. move_semantics1:绑定默认不可变,mut 显式声明;
  2. move_semantics2:按值传参即移动,需要原值时 clone()
  3. move_semantics3:参数本身就是绑定,直接 mut 声明更简洁;
  4. move_semantics4:可变引用排他 + 借用生命周期以最后使用为准(NLL);
  5. move_semantics5:按“是否要拿走值”决定传引用还是传所有权。

验证与自查资源都在仓库内:每道题先让 rustlings watch 模式复现编译错误,再对照 solutions/06_move_semantics 下的解法(rustlings 在提示完成时会输出“Solution for comparison”一行的解法路径,实现见 src/exercise.rssolution_link_line);提示不够用时按 h 读取 rustlings-macros/info.toml 中为每道题准备的 hint 文本。理论部分建议按 README 指引精读 Rust 官方书第 4.1(Ownership)与 4.2(References and borrowing)两节——本章五道练习几乎可以逐条对应到这两节的规则上,这也是 rustlings 用“最小可复现错误”来教学所有权的设计初衷。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384