Rust 系统学习笔记(一):从所有权到可测试的 Cargo 工程

这份笔记要解决什么

Rust 的语法并不是最难的部分。真正容易卡住的是:已经知道 letstructmatch,但一写项目就不断遇到 move、borrow、lifetime 和 trait 报错,于是开始到处加 clone(),或者把所有类型都包进智能指针。

这份第一版笔记不按语法手册逐项抄录,而是围绕一条小型数据链路建立统一模型:

文本输入
↓ Result:解析可能失败
Vec<Sample>
↓ &T:只读统计不取得所有权
Option<JointStats<'_>>
↓ main:决定怎样向用户报告“成功 / 不存在 / 失败”
命令行输出

配套的 rust_learning_lab 是一个没有第三方依赖的 Cargo crate:读取几行机器人关节位置,解析为结构体,并计算某个关节的统计量。它很小,但足以贯穿所有权、借用、生命周期、OptionResult、模块、测试和异步的最小心智模型。

本文写于 2026-09-13,以 Rust 2024 Edition 为语言基线。配套项目将 rust-version 设为 1.85,因为 Rust 1.85 是 2024 Edition 的首个稳定版本;学习时可以使用更新的 stable 工具链。Edition 不是编译器版本,也不是生态分叉:每个 crate 可以独立选择 edition,不同 edition 的 crate 仍可互操作。

先看学习地图

阶段 核心问题 本文入口 通过标准
0. 工具链 怎样获得可重复的构建环境? Rustup 与 Cargo 能解释 rustc、Cargo、toolchain、edition、MSRV 的区别
1. 数据建模 怎样用类型表达有效状态? 语法与类型 structenummatch 写出小模型
2. 所有权 谁负责释放值?调用后还能不能用? 所有权 能预判 move、Copyclone 和作用域结束的位置
3. 借用与生命周期 函数能否只看或暂时修改数据? 借用生命周期 先设计输入输出关系,再读懂 E0382、E0499、E0502
4. 失败建模 “没有”和“失败”是否一回事? Option 与 Result 库层不靠 unwrap() 处理外部输入
5. 工程组织 package、crate、module 是什么关系? Cargo 工程 能拆出 lib/bin、单元测试和集成测试
6. 并发边界 async 到底改变了什么? 异步入门 知道 future、.await、executor 与线程不是同义词
7. 系统集成 什么时候才需要 unsafe 与 FFI? FFI 边界 把 unsafe 压缩在小模块内,并提供安全包装与测试

学习顺序可以迭代,但不建议跳过所有权直接背生命周期标注。生命周期解决的是引用之间的有效期关系;如果还没分清“拥有”和“借用”,'a 只会变成另一种符号记忆。

1. Rustup、rustc 与 Cargo 各管什么

官方推荐用 Rustup 管理 Rust 工具链。安装完成后,常见的三个命令分别承担不同职责:

工具 责任 常用命令
rustup 安装、选择和更新 toolchain / target / component rustup toolchain install stablerustup show
rustc 编译单个 crate,给出语言级诊断 rustc --versionrustc --explain E0382
cargo 管理 package、依赖、构建、运行、测试和发布 cargo checkcargo testcargo run

第一次准备环境时,可以按官方安装页完成 Rustup 安装,然后执行:

rustup toolchain install stable --component rustfmt --component clippy
rustup default stable
rustc --version
cargo --version

不要在还没看清脚本来源时复制任意“curl 后直接执行”的安装命令;本文只维护工具的使用方法,安装入口以 Rust 官方安装页 为准。

Edition、stable 与 MSRV 是三件事

配套项目的清单包含:

[package]
name = "rust-learning-lab"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
publish = false
  • edition = "2024" 选择同一门 Rust 语言的一组兼容性规则;
  • stable 是 Rustup 管理的发布通道,会随升级移动到更新编译器;
  • rust-version = "1.85" 声明这个 package 支持的最低 Rust 版本,也称 MSRV;
  • publish = false 防止这个练习包被误发到 crates.io

新项目最好明确写出 edition。若 Cargo.toml 完全省略它,Cargo 为兼容历史项目会按 2015 Edition 处理,这和当前 cargo new 默认生成最新版 edition 不是一回事。

日常反馈环

把“写完再编译”缩短成以下循环:

cargo check
cargo test
cargo fmt --all --check
cargo clippy --all-targets -- -D warnings

cargo check 会完成类型检查和借用检查,但通常不生成最终机器码,所以适合高频运行;cargo test 才是行为验收。fmt 统一格式,Clippy 补充编译器之外的惯用写法检查。四者职责不同,不能互相替代。

2. 基础语法的重点:用类型缩小无效状态

先看一段完整但很小的建模代码:

#[derive(Debug, Clone, PartialEq)]
pub struct Sample {
pub joint: String,
pub position_rad: f64,
}

pub enum ParseErrorKind {
MissingComma,
EmptyJoint,
InvalidPosition(String),
NonFinitePosition,
}

这里已经包含几项基础规则:

  • 变量和字段默认不可变,需要修改时显式写 mut
  • struct 把同时存在的数据组织成乘积类型;
  • enum 表示几个互斥分支,是和类型;每个分支还可以携带自己的数据;
  • String 拥有一段可增长的 UTF-8 文本,&str 是对 UTF-8 文本的借用视图;
  • #[derive(...)] 让编译器为类型生成可调试输出、克隆和相等比较等 trait 实现。

表达式、语句与返回值

Rust 的 ifmatch 和代码块都可以是表达式。函数最后一个表达式不写分号时,就是返回值:

fn classify(position_rad: f64) -> &'static str {
if position_rad.abs() < 0.01 {
"near zero"
} else {
"offset"
}
}

"near zero" 后加分号会把这个分支变成 (),从而与函数声明的 &str 不匹配。初学时看到“expected &str, found ()”,先检查尾表达式是否被分号变成了语句。

match 应该穷尽状态

match 要覆盖所有可能分支:

fn error_hint(kind: &ParseErrorKind) -> &'static str {
match kind {
ParseErrorKind::MissingComma => "检查 CSV 分隔符",
ParseErrorKind::EmptyJoint => "补上关节名",
ParseErrorKind::InvalidPosition(_) => "位置必须是数字",
ParseErrorKind::NonFinitePosition => "拒绝 NaN 和无穷大",
}
}

这不是语法负担,而是演进提醒:将来新增错误类型时,编译器能指出哪些地方还没决定如何处理。若用 _ 一口吞掉所有剩余分支,就会失去这种帮助;只有当剩余值确实同义时再使用通配模式。

String&str 与集合的默认选择

一个实用的接口经验是:

  • 结构体需要长期保存文本时,先考虑拥有的 String
  • 函数只读字符串时,优先接收 &str,它同时兼容字符串字面量和 String 的切片;
  • 默认可变长序列先考虑 Vec<T>
  • 不要因为借用暂时难写,就给所有字段加 'static 或把所有数据泄漏到全局。

3. 所有权:先问“谁负责释放”

Rust 的所有权规则可以压缩成三句话:

  1. 每个值都有一个当前 owner;
  2. 同一时刻只有一个 owner;
  3. owner 离开作用域时,该值会被 drop

StringVec<T> 这类拥有堆资源的值,普通赋值通常是 move,不是深拷贝:

let samples = vec![Sample::new("hip", 0.4)];
let archived = samples;

println!("{}", archived.len());
// println!("{}", samples.len()); // E0382: value used after move

移动后仍然只有一个 owner,因此不会发生两个变量重复释放同一资源。移动通常只复制栈上的指针、长度和容量等小型元数据,不会自动复制整段堆数据。

Rust 中所有权、共享借用、可变借用与移动的关系

一段程序可以先共享读取、再独占修改、最后移动所有权;关键是这些访问阶段不要非法重叠。引用不负责释放被借用的值。

Copy、move 与 clone()

整数、布尔值和由 Copy 类型组成的简单值常常实现 Copy

let count = 3_u32;
let another = count;
println!("{count} {another}");

赋值后两者都可用,因为按位复制就是完整且安全的复制。String 没有实现 Copy,需要真实复制内容时必须显式 clone()

let joint = String::from("front_left_hip");
let label = joint.clone();

clone() 不是“修复 borrow checker 的万能键”。在循环、图结构或大缓冲区上,它可能改变性能和数据语义。每次添加前问:

  • 这里真的需要两个独立拥有者吗?
  • 函数是否只需接收 &str&[T]
  • 能否让被调用者返回所有权,或者调整数据拥有关系?
  • 如果复制是产品语义,是否应该在函数名和测试中体现?

函数签名就是所有权协议

fn inspect(samples: &[Sample]) { /* 只读借用 */ }
fn calibrate(samples: &mut [Sample]) { /* 独占修改 */ }
fn archive(samples: Vec<Sample>) { /* 取得所有权 */ }
fn create() -> Vec<Sample> { /* 把所有权交给调用者 */ }

只看签名,就应该能判断调用后原值是否还能使用。Rust API 设计的第一步不是写函数体,而是先决定这四种关系中的哪一种准确表达意图。

4. 借用:多读或一写

创建引用称为借用。最重要的规则是:在同一段有效时间里,可以有任意多个共享引用 &T,或者一个可变引用 &mut T;两组不能重叠,并且引用必须始终有效。

let mut samples = vec![Sample::new("hip", 0.4)];

let first_read = &samples;
let second_read = &samples;
println!("{} {}", first_read.len(), second_read.len());

// 两个共享引用最后一次使用已经结束,下面才开始独占借用。
let only_writer = &mut samples;
only_writer[0].position_rad += 0.1;

借用的有效范围通常到引用的最后一次使用,不必机械地持续到整个花括号结束。这就是为什么上面先读后写可以通过编译。

接口优先借用切片

只需要查看一组元素时,&[Sample] 通常比 &Vec<Sample> 更通用:

pub fn latest_for<'a>(samples: &'a [Sample], joint: &str) -> Option<&'a Sample> {
samples.iter().rev().find(|sample| sample.joint == joint)
}

调用者可以传入 Vec 的全部或局部切片,也可以传数组切片。函数不关心集合如何扩容,只关心“连续的一段 Sample”。可变版本相应写成 &mut [Sample]

常见借用错误怎么读

错误码 常见含义 首先检查
E0382 值已经 move,之后又被使用 哪个调用取得了所有权;是否本应传引用
E0499 同一个值被同时可变借用多次 第一个 &mut 的最后一次使用在哪里
E0502 不可变借用与可变借用重叠 能否把“读”和“写”拆成两个阶段
E0515 / E0597 返回或保存的引用活得比来源久 是否应该返回拥有的值;来源是否在函数内被释放

先画 owner 与引用的来源,再改签名。不要看到生命周期错误就盲目添加 'a,也不要先上 Rc<RefCell<_>> 绕过静态检查。

5. 生命周期:描述引用关系,不是延长变量寿命

每个引用都有生命周期,大多数时候编译器可以推断。需要显式标注时,标注表达的是“输入引用与输出引用之间有什么关系”,不会让任何值活得更久。

配套项目中:

pub fn latest_for<'a>(samples: &'a [Sample], joint: &str) -> Option<&'a Sample> {
samples.iter().rev().find(|sample| sample.joint == joint)
}

'a 的含义是:如果返回 Some(&Sample),其中的引用来自 samples,不能比 samples 的借用更久。joint 只用于比较,不可能成为返回值来源,所以它不需要与 'a 绑定。

为什么下面无法返回引用

fn invalid() -> &str {
let name = String::from("hip");
&name
}

name 在函数末尾被释放,返回的引用将悬空。加 'static 不能把局部 String 变成静态数据。正确选择通常是返回拥有的 String

fn valid() -> String {
String::from("hip")
}

只有字符串字面量、真正的静态项,或者有意长期保留的数据才适合 'static。把 'static 当作“最长所以最好”会把资源释放、测试隔离和接口复用都变差。

结构体为什么有生命周期参数

pub struct JointStats<'a> {
pub joint: &'a str,
pub count: usize,
pub mean_rad: f64,
}

这个结果不复制关节名,而是借用某个 Sample 内的字符串。因此 JointStats 不能脱离原 samples 长期保存。若要跨线程队列、缓存或异步任务长期持有它,更自然的设计可能是把字段改成 String,让结果拥有名字。借用版减少复制,拥有版降低耦合;选择取决于数据需要活多久,而不是哪一种更“高级”。

6. OptionResult 与错误边界

Rust 用类型区分两种经常被混在一起的情况:

  • Option<T>:值可能存在,也可能正常地不存在;
  • Result<T, E>:操作可能成功,也可能带着原因失败。

在练习项目中,“目标关节没有采样”是 None,而“一行 CSV 不是合法数字”是 Err(ParseError)。如果两者都用空字符串或 -1 表示,调用者无法可靠地区分业务空值和数据损坏。

let samples = parse_samples(&input)?;

match summarize_joint(&samples, &joint) {
Some(stats) => println!("mean: {}", stats.mean_rad),
None => println!("joint `{joint}` was not found"),
}

? 做了什么

Result 使用 ? 时:

  1. 若为 Ok(value),取出 value 继续执行;
  2. 若为 Err(error),把错误转换为当前函数声明的错误类型并提前返回。

它不是忽略错误,也不是 panic。? 迫使当前函数本身也有一个能表达失败的返回类型。对 Option 使用时同理:None 会提前传播 None

库层返回信息,应用边界决定呈现

配套项目为解析错误保存物理行号和具体原因:

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
pub line: usize,
pub kind: ParseErrorKind,
}

错误类型实现 Display 供用户阅读,实现 Error 供错误链组合。解析库不擅自退出进程;main、HTTP handler 或 ROS 节点边界再决定日志、退出码、重试或降级策略。

什么时候可以 unwrap()

谨慎使用,但不必把它绝对禁止:

  • 单元测试中,失败本来就应该立即暴露;
  • 编译器不能证明、但程序已经由紧邻逻辑保证的内部不变量,可用带解释的 expect()
  • 外部输入、文件、网络、用户参数和传感器数据不应靠 unwrap() 处理。

若错误确实不可恢复,可以 panic;但“懒得建模”不等于“不可恢复”。

7. trait、泛型与迭代器:先表达能力,再复用实现

impl Into<String>Sample::new 同时接收字符串字面量和 String

impl Sample {
pub fn new(joint: impl Into<String>, position_rad: f64) -> Self {
Self {
joint: joint.into(),
position_rad,
}
}
}

这里的重点不是追求最抽象,而是让边界接收多种可转换输入,同时在结构体内部统一拥有 String。泛型和 trait bound 应来自实际复用需求;只有一个实现时,不必为了“设计模式”提前制造层级。

解析器使用迭代器把数据流写成几步变换:

input
.lines()
.enumerate()
.filter_map(/* 跳过空行和注释,同时保留物理行号 */)
.map(/* 每一行 -> Result<Sample, ParseError> */)
.collect::<Result<Vec<_>, _>>()

collect 在这里会遇到第一个 Err 时停止并返回错误;全部成功才得到 Ok(Vec<Sample>)。迭代器通常是惰性的:构造适配器不会自动执行,消费器(如 collectsumfor_each)才推动数据流。

不要为了“一行链式调用”牺牲诊断信息。如果闭包太复杂,就提取成命名函数;配套项目把单行解析单独放到 parse_line,便于测试每一种失败。

8. Cargo 工程:package、crate、module 不要混用

这三个词处在不同层级:

概念 含义 本项目对应物
package 一个 Cargo.toml 描述的发布与构建单元 rust-learning-lab/
crate 一次编译形成的库或可执行目标 src/lib.rs 的 library crate;src/main.rs 的 binary crate
module crate 内部的命名空间与可见性结构 modelparserreportasync_intro

项目结构如下:

rust_learning_lab/
├── Cargo.toml
├── Cargo.lock
├── data/samples.csv
├── src/
│ ├── lib.rs
│ ├── main.rs
│ ├── model.rs
│ ├── parser.rs
│ ├── report.rs
│ └── async_intro.rs
├── examples/
│ ├── ownership.rs
│ └── async_intro.rs
└── tests/
└── telemetry_workflow.rs

src/lib.rs 是公开 API 的门面:

pub mod async_intro;
pub mod model;
pub mod parser;
pub mod report;

pub use model::{JointStats, Sample};
pub use parser::{ParseError, ParseErrorKind, parse_samples};
pub use report::{apply_offset, latest_for, summarize_joint};

mod 把文件纳入模块树,pub 决定外部可见性,use 只是在当前作用域引入名字,pub use 则可以重新导出、塑造更稳定的公共入口。文件夹结构不应该机械等同于公开 API 结构。

为什么同时保留 lib 与 bin

把解析和统计放入 library crate 后:

  • 命令行程序只处理参数、文件 I/O 和输出;
  • 集成测试可以像外部使用者一样调用公开 API;
  • 以后接 HTTP、ROS2 或 FFI 时可复用核心逻辑;
  • main.rs 不会膨胀成无法隔离测试的全部实现。

单元测试、集成测试与示例

  • 模块里的 #[cfg(test)] mod tests 可以检查私有细节,适合边界条件;
  • tests/*.rs 各自是独立 crate,只能通过公开 API 验收工作流;
  • examples/*.rs 是可运行的学习材料,cargo test --all-targets 还会确保它们持续可编译;
  • 文档代码块如果需要长期保证,也应逐步改成可执行 doctest,而不是只相信文章渲染成功。

运行配套项目:

cd source/downloads/code/rust_learning_lab
cargo run
cargo run -- data/samples.csv front_left_hip
cargo run --example ownership
cargo run --example async_intro
cargo test --all-targets

默认输出应为:

joint: front_left_hip
count: 3
mean_rad: 0.440
range_rad: 0.420..=0.460
latest_rad: 0.440

注意 cargo run -- <参数> 中的 --:它把 Cargo 自己的参数与传给目标程序的参数分开。

9. 异步入门:async 产生 Future,不会自动产生并发

async fn 调用后返回一个实现 Future 的值。Future 表示“可能尚未完成的计算”,只有被轮询时才取得进展;.await 允许当前异步任务在等待时把执行机会让给别的任务。

pub async fn summarize_async<'a>(samples: &'a [Sample], joint: &str) -> Option<JointStats<'a>> {
summarize_joint(samples, joint)
}

这个函数没有真正等待 I/O,所以只是用来观察类型和生命周期如何进入 Future。标准库定义了 FuturePollContextWaker 等底层协议,但常规网络/定时器应用还需要异步运行时负责 executor、I/O reactor、计时器和任务调度。

配套项目中的 run_ready 只轮询一次,并在得到 Poll::Pending 时直接失败。它能证明“调用 async 函数得到 Future,executor 对 Future 进行 poll”这个最小关系,不是通用 executor,也不能拿来做网络、文件或定时任务

async、线程与并行的区别

概念 主要解决 不自动保证
async task 大量等待型工作的协作调度 CPU 并行、阻塞调用自动变非阻塞
OS 线程 抢占式调度、可利用多个核心 低内存开销、无数据竞争
数据并行 把可分工作分配到多个核心 I/O 并发、确定的完成顺序

CPU 密集计算写成 async fn 不会自动变快;在 executor 线程里调用阻塞 API 还可能拖住其他任务。先识别工作是在等待 I/O,还是在持续计算,再选模型。

上真实运行时前要补的四个问题

  1. 哪些资源会跨过 .await,它们是否仍然有效?
  2. task 是否需要跨线程移动,从而要求 Future 为 Send
  3. 超时、取消和部分副作用如何处理?丢弃 Future 不会撤销已经完成的外部写入;
  4. 阻塞的 C/C++、文件或计算调用放在哪里,是否需要专用阻塞线程池?

第一版先稳定同步核心。等确实出现多个 socket、定时器或高并发连接,再选择运行时,并把运行时依赖限制在应用边界。

10. C/C++ FFI:把 unsafe 当作待证明的边界

与现有 C/C++ 或系统组件对接时,不要把 Rust 的 StringVec<T> 或默认布局结构体直接跨 ABI 传递。常见边界是:

#[repr(C)]
pub struct JointSample {
pub joint_id: u32,
pub position_rad: f64,
}

unsafe extern "C" {
fn read_joint_sample(output: *mut JointSample) -> i32;
}

2024 Edition 中 extern block 必须写成 unsafe extern,因为编译器无法验证另一侧函数的真实契约。#[repr(C)] 为结构体选择 C 兼容的布局规则;它不自动解决对齐、整数宽度、字节序、所有权和线程安全。

C++ 侧应提供窄的 extern "C" 包装层,避免把名称修饰、异常、STL 容器和对象生命周期直接暴露给 C ABI。Rust 侧再写安全包装函数,把原始返回码转成 Result,把裸指针的有效性、不为空、长度、对齐和生命周期检查集中在一个小模块里。

FFI 验收清单

  • ABI 是否固定为 C,结构体是否使用 #[repr(C)]
  • 每个指针由谁分配、谁释放、允许为空吗、有效长度是多少;
  • C 字符串是否 NUL 结尾,是否可能包含内部 NUL,编码是什么;
  • 回调在哪个线程执行,能否重入,context 指针活多久;
  • C++ 异常是否被挡在 extern "C" 边界内;
  • Rust panic 是否被挡在导出边界内;
  • 构建脚本是否明确链接库、架构、搜索路径和静态/动态链接方式;
  • 是否有正常、空指针、边界长度、错误码和析构路径测试;
  • 是否用 sanitizer、Miri 或平台诊断工具补充检查;这些工具各有适用边界,不能把单次通过当成证明。

当前练习 crate 没有真实 FFI 需求,因此不引入 unsafe。等出现明确的 C API、头文件、目标架构和生命周期契约时,再建立独立 companion crate;这比为了覆盖名词而写一个无实际消费方的裸指针 demo 更可靠。

11. 一套可持续的质量门禁

建议把 Rust 项目的本地门禁固定成:

cargo fmt --all --check
cargo clippy --all-targets -- -D warnings
cargo test --all-targets
cargo doc --no-deps

若 package 声明 MSRV,还要在那个版本上至少运行 cargo check 与测试。stable 上通过不能证明 MSRV 仍然可用;反过来,MSRV 通过也不能发现最新编译器新增的 lint 或未来不兼容警告。

本版验证记录(2026-09-13)

本机没有全局安装 Rust;为避免改变宿主环境,配套 crate 在官方 Rust 容器内验证:

工具链 执行项 结果
Rust 1.85.1 / Cargo 1.85.1 test --all-targetsfmt --check、Clippy -D warnings、main 与两个 example 5 个测试通过,格式和 lint 通过,三个入口输出符合预期
Rust 1.98.1(验证时的 stable 补丁版) test --all-targets 同一组 5 个测试通过

这证明当前源码同时满足声明的 MSRV 与验证时 stable;不代表未来 stable、其他 target、真实异步 I/O 或尚未建立的 FFI 链路已经通过。

编译错误也可以成为练习

所有权有一类重要知识只能从“故意失败”的代码中学习。不要把失败示例放进默认 target 破坏整套构建,可以采用:

  1. examples/ownership.rs 中保留一行注释掉的 E0382 示例;
  2. 复制到临时文件,取消注释;
  3. 运行 cargo check --example ownership
  4. 阅读完整诊断,再执行 rustc --explain E0382
  5. 分别用“借用”“返回所有权”“确实 clone”三种方式修复,比较 API 含义。

目标不是消灭红字,而是学会从诊断中还原所有权图。

12. 六个迭代阶段,而不是一次读完

下面是一条可持续的首轮路线。每个阶段都以可运行结果结束,不用按自然周硬性推进。

阶段 学习与修改任务 产物 / 验收
A. 建模 读基础语法、struct、enum、pattern;给 Sample 增加时间戳 编译通过;新增构造与边界测试
B. 所有权 运行 ownership example;分别触发 E0382、E0499、E0502 用自己的话画出 owner、borrow 与最后使用点
C. 错误 为额外列、非法单位设计错误;禁止库层 unwrap 外部输入 错误包含行号与原因;集成测试覆盖失败路径
D. 抽象 增加单位转换 trait 或泛型输入,但保持公开 API 小 Clippy 零警告;能说明为何需要该抽象
E. 工程 拆 lib/bin;补 example、integration test、文档 fmt + clippy + test + doc 全通过
F. 集成 有真实等待再接 async runtime;有真实 C API 再接 FFI 记录依赖选择、取消/所有权或 ABI 契约及端到端测试

每次学习记录只回答六个问题

日期 / toolchain:
目标:今天只验证哪一个假设?
最小改动:改了哪个类型、签名或测试?
编译器反馈:错误码或 lint 是什么?
我的解释:哪个值拥有数据,哪个引用来自哪里,错误如何传播?
验证:运行了哪些命令,结果是什么?
下一步:只留一个最小问题。

这种记录比“今天看完生命周期”更可复查。Rust 的理解往往体现在接口取舍和编译器反馈里,不体现在读了多少页。

13. 第一轮练习题

按顺序修改配套项目,每题都先加测试:

  1. 为输入增加 timestamp_ms,决定它用 u64 还是新类型,并解释选择;
  2. 让统计函数支持任意关节前缀,返回的名字应该借用输入还是拥有新字符串?
  3. position_rad 的单位误写成 degree 时返回结构化错误,不用字符串包含判断做业务分支;
  4. 增加 median。排序时是否需要 clone 整个 Sample,还是只复制 f64
  5. 写一个函数同时返回最小值和最大值的样本引用,标清输出生命周期来源;
  6. 将文件读取抽到应用边界,保证 parser 只接收 &str,然后用内存 fixture 测试;
  7. 找到真实的异步 I/O 需求后,再比较“同步核心 + 异步外壳”与“全部 async”的复杂度;
  8. 找到真实 C API 后,先写一页 ABI/所有权契约,再写第一行 unsafe extern

完成一道题的标准不是“能跑”,而是能用一句话说清楚:谁拥有数据、谁在借用、可能怎样失败、哪条测试证明行为。

14. 当前边界与后续主题

这份 v1 已覆盖 TODO 中的基础主线,但它不是 Rust 百科。以下内容适合作为后续 companion notes,而不是继续把所有概念堆进本文:

  • 智能指针:BoxRcArcCellRefCellMutex 的所有权与运行时成本;
  • trait object、关联类型、闭包与更系统的泛型 API 设计;
  • async runtime、stream、背压、结构化并发、取消与 tracing;
  • unsafe 的有效性不变量、Miri、sanitizer 与并发模型;
  • CMake / ROS2 / ONNX Runtime 的真实 FFI companion crate;
  • workspace、feature、build script、交叉编译、供应链和发布策略。

继续学习时仍沿用本文的主线:先明确问题与数据寿命,再设计类型和签名,然后让测试与工具链给出证据。

官方资料

配套源码入口:Cargo.tomlsrc/lib.rssrc/parser.rssrc/report.rstests/telemetry_workflow.rs

3d打印 actor-critic adaptive sampling ai辅助设计 algorithm algorithms anymal apriltag ardupilot async atlas attention automation axis-angle bang-bang belief encoder bitbake blender bode bsp c++ cadquery calibration camera calibration camera-intrinsics cargo chrome cmake cmakelists cnn colcon computer-vision conan control controller_manager cpp cpu d435i dagger data_struct db depth camera depth-camera design-pattern diagnostics direct collocation dots dtof economics eigen elevation map elf embedded linux executor factory-pattern fcpx ffi fiducial marker figure finance forge fourier fov freecad gae gazebo gdb geometry git gnu gru guitar hardware humanoid ibus imu interest isaac gym isaac lab isaaclab kdl laplace latent variable latex launch learning-notes legged locomotion legged robotics legged-robot legged_gym life linux linux-kernel linux-tools mac math matlab matrix memory mixture of experts mlp money motion imitation motion-control motor moveit mpc mujoco music-theory network neural mapping ocs2 ode onnx openscad operator optimal algorithm optimal-control perceptive locomotion perf performance personal-finance piano pinhole-camera pinocchio pixhawk pixhawk 6c point-cloud policy distillation ppo privileged learning profiling px4 python qgroundcontrol qos quadrotor qualcomm realsense reinforcement learning representation learning reward tuning rnn robogauge robot robot parkour robotics ros ros2 ros2_control rsl_rl rtb rust security sensor-fusion shell signal-processing sim-to-real simulation socket soft dynamics constraints spot ssh stairs stl stm32 systems-programming tcp-ip teacher policy teacher student teacher-student temporal convolution terrain reconstruction thread tools tron1 twist ubuntu uml uncertainty unitree unitree g1 urdf vae valgrind vcxsrv velocity vim web wifi wiring work workflow wsl yocto z-transform zero-shot transfer 中文输入 交叉编译 人形机器人
知识共享许可协议