附录 C · 语法与标准库速查卡
本附录是边写代码边查的高密度速查卡:全部以表格与极短代码片段组织,不展开讲解。 需要原理与推导请回到对应正文章节;术语译法统一遵循 附录 A · 术语中英对照。
基准环境:
rustc 1.98.1/cargo 1.98.1,Rust 2024 edition,Windows 11 + PowerShell。 片段中标注的最小稳定版本(如 1.75)表示该写法在该版本起可用。
怎么用这张卡
| 你在做什么 | 直接跳 |
|---|---|
| 忘了语法的「骨架」长什么样 | C.1 语法骨架 |
println! 里那串花括号不会写 | C.2 格式化输出 |
| 不确定该用哪个整数类型 / 怎么转 | C.3 类型 |
| 想找某个集合的方法名与复杂度 | C.4 集合操作 |
| 数据要「一条链」处理完 | C.5 迭代器 |
少写几层 match | C.6 Option / Result |
| 多线程 / async 样板代码 | C.7 智能指针与并发 |
| 读写文件、跑子进程、拿参数 | C.8 文件与 IO |
记不住 cargo 子命令与 Cargo.toml 字段 | C.9 Cargo |
| 不知道某个 trait 何时该实现 | C.10 常用 trait |
#[...] 属性什么意思 | C.11 属性与 lint |
C.1 语法骨架速查
C.1.1 crate / mod / use / pub 路径语法
| 骨架 | 写法 | 备注 |
|---|---|---|
| crate 根 | src/main.rs(二进制)或 src/lib.rs(库) | 一个包(package)最多一个库 target,可有多个 bin |
| 内联模块 | mod m { ... } | 花括号内的项归 m 所有 |
| 单文件模块 | mod m; + src/m.rs | 2024 edition 下二选一,不再需要 mod.rs |
| 目录模块 | mod m; + src/m/mod.rs 或 src/m.rs | 有子模块时推荐 src/m.rs + src/m/sub.rs |
| 绝对路径 | crate::a::b / ::std::mem::swap | crate:: 指当前 crate 根;::std 指外部 crate |
| 相对路径 | self::a::b / super::c / super::super::d | self 是本模块,super 是父模块 |
| 引入 | use crate::a::b; | 必须显式 use,Rust 没有隐式导入 |
| 重命名 | use std::io::Result as IoResult; | 解决同名冲突 |
| 批量与通配 | use std::collections::{HashMap, HashSet}; / use prelude::*; | 通配仅建议用于 prelude 或测试 |
| 重导出 | pub use crate::a::b; | 让外部通过本模块路径访问,构建 facade |
| 可见性 | pub / pub(crate) / pub(super) / pub(in crate::a) / 默认私有 | 默认私有;父模块看不到子模块的私有项 |
| 字段可见性 | pub struct P { pub x: i32, y: i32 } | 逐字段标注;y 只能由本模块构造 |
| 枚举变体 | pub enum E { A, B(i32) } | 变体继承枚举的可见性,不能单独标 pub |
rust
// 一个最小但完整的模块骨架:注意 use 与 mod 的分工
mod geometry {
pub struct Point {
pub x: f64,
y: f64, // 私有字段:外部无法直接构造或读取
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y } // 同一模块内可访问私有字段
}
pub fn y(&self) -> f64 {
self.y
}
}
pub mod shape {
use super::Point; // super = geometry
pub fn origin() -> Point {
Point::new(0.0, 0.0)
}
}
}
fn main() {
use geometry::{shape, Point};
let p = shape::origin();
println!("{} {}", p.x, p.y()); // 输出:0 0
}💡 对照:Python 的
import是运行期导入模块对象,Java 的package靠目录约定。 Rust 的mod是编译期的项树,use只是给树上的节点起个短名字 —— 所以use不产生任何运行期开销,也不能「动态导入」。
C.1.2 变量、可变性与绑定模式
| 骨架 | 写法 | 备注 |
|---|---|---|
| 不可变绑定 | let x = 1; | 默认不可变,这是 Rust 的默认值 |
| 可变绑定 | let mut x = 1; | mut 是绑定的属性,不是值的属性 |
| 类型标注 | let x: i64 = 1; | 多数情况可推断,边界处(如 parse)必须写 |
| 常量 | const MAX: u32 = 100; | 编译期求值,可内联到任意作用域,必须标类型 |
| 静态 | static NAME: &str = "rust"; | 有固定地址,'static 生命周期 |
| 可变静态 | static mut X: i32 = 0; | 2024 edition:访问需 unsafe,改用 AtomicI32 |
| 遮蔽 | let x = x + 1; | 新绑定覆盖旧名字,可换类型 —— 与 mut 不同 |
| 元组解构 | let (a, b) = (1, 2); | 位置对应 |
| 结构体解构 | let Point { x, y } = p; | 字段名简写;Point { x: a, .. } 忽略其余 |
| 忽略 | let _ = f(); / let (_, b) = t; | _ 立即丢弃;_x 只是不加警告的普通绑定 |
| 延迟初始化 | let x; if c { x = 1 } else { x = 2 } | 所有分支恰好赋值一次即可 |
| let-else(1.65) | let Some(v) = o else { return; }; | else 分支必须发散(return/panic!/continue) |
| if let | if let Some(v) = o { ... } else { ... } | 单分支匹配的语法糖 |
| while let | while let Some(v) = it.next() { ... } | 循环直到模式不匹配 |
rust
fn main() {
let x = 5;
let x = x * 2; // 遮蔽:类型可变,与 mut 完全不同
let x = "now a string"; // 合法:又一个新绑定
println!("{x}"); // 输出:now a string
let mut count = 0;
count += 1; // 需要 mut 才能改
let parsed: Result<i32, _> = "42".parse();
let Ok(n) = parsed else {
println!("解析失败");
return;
};
println!("{count} {n}"); // 输出:1 42
}⚠️ 陷阱:
mut与遮蔽解决的是两个不同问题。 需要原地修改用mut;需要换类型或换语义用遮蔽。 反复let x = ...不会释放旧值,旧值照常在其作用域结束时 drop。
C.1.3 函数签名
| 骨架 | 写法 |
|---|---|
| 最简 | fn f() {} |
| 参数 | fn f(a: i32, b: &str) {} —— 形参类型必须标注 |
| 返回 | fn f() -> i32 { 1 } —— 最后表达式即返回值,不加分号 |
| 提前返回 | fn f(x: i32) -> i32 { if x < 0 { return 0; } x } |
| 无返回 | -> () 可省略,末尾表达式为 () |
| 发散返回 | fn f() -> ! { panic!("never") } |
| 泛型 | fn f<T>(v: T) -> T { v } |
| 借用参数 | fn f(s: &str, v: &mut Vec<i32>) —— 首选 &str/&[T] 而非 &String/&Vec<T> |
| 带生命周期 | fn f<'a>(x: &'a str) -> &'a str { x } |
| where 子句 | fn f<T>(v: T) -> usize where T: AsRef<str> { v.as_ref().len() } |
| 函数指针类型 | let g: fn(i32) -> i32 = double; |
| 高阶函数 | fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) } |
| 返回闭包 | fn make() -> impl Fn(i32) -> i32 { |x| x + 1 } |
| 方法 | impl S { fn m(&self) -> i32 { 0 } } |
| 关联函数 | impl S { fn new() -> Self { S } } —— 无 self,用 S::new() 调用 |
| 可变方法 | fn m(&mut self) —— 需要调用方持有 mut 绑定 |
| 消费方法 | fn into_inner(self) -> i32 —— 夺走所有权 |
rust
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
fn describe<T>(v: T) -> String
where
T: std::fmt::Debug,
{
format!("{v:?}")
}
fn main() {
println!("{:?}", divide(6.0, 3.0)); // 输出:Some(2.0)
println!("{}", longest("abc", "ab")); // 输出:abc
println!("{}", describe(vec![1, 2, 3])); // 输出:[1, 2, 3]
}⚠️ 陷阱:
fn f() -> i32 { 1; }里的分号会让函数体值为(),报E0308 mismatched types。 这是 Rust 新手最高频的错误,记住:最后一行要以表达式结尾,不要分号。
C.1.4 struct / enum / impl / trait 最小骨架
rust
// 三种结构体
struct Unit; // 单元结构体:只做标记
struct Meters(f64); // 元组结构体:新类型(newtype)常用
struct User { // 命名字段结构体
name: String,
age: u32,
}
// 枚举:变体可携带不同类型的数据
enum Shape {
Circle { r: f64 }, // 结构体变体
Rect(f64, f64), // 元组变体
Empty, // 单元变体
}
// 给枚举挂方法:match 必须覆盖全部变体
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { r } => std::f64::consts::PI * r * r,
Shape::Rect(w, h) => w * h,
Shape::Empty => 0.0,
}
}
}
// trait:定义共享行为
trait Area {
fn area(&self) -> f64; // 必须实现
fn is_flat(&self) -> bool { // 默认方法,可被覆盖
self.area() == 0.0
}
}
impl Area for Shape {
fn area(&self) -> f64 {
Shape::area(self) // 注意:需用全限定名避免递归
}
}
// trait 约束泛型 / impl Trait / dyn Trait 三种写法
fn print_area<T: Area>(x: &T) { println!("{}", x.area()); }
fn print_area2(x: &impl Area) { println!("{}", x.area()); }
fn print_area3(x: &dyn Area) { println!("{}", x.area()); }
fn main() {
let s = Shape::Rect(3.0, 4.0);
print_area(&s);
print_area2(&s);
print_area3(&s);
println!("{}", s.is_flat()); // 输出:12 / 12 / 12 / false
}| 选择 | 何时用 | 代价 |
|---|---|---|
impl Trait(参数位置) | 只接受一种具体类型,追求静态分发 | 单态化,代码体积增大 |
<T: Trait> | 需要在函数体内命名 T(如返回 T) | 同上 |
&dyn Trait | 需要异构集合(Vec<Box<dyn Trait>>)、减少编译时间 | 虚表跳转、无法内联 |
Box<dyn Trait> | 需要持有所有权且类型擦除 | 堆分配 + 虚表 |
🧠 原理:
impl Trait与dyn Trait是「静态分发 vs 动态分发」的选择, 在 C++ 里对应模板与虚函数,在 Go 里对应泛型与 interface。 Rust 让两种写法同时存在且可混用,但不能在同一个返回类型里混用。
C.1.5 泛型与 where
| 骨架 | 写法 |
|---|---|
| 单参数 | fn f<T>(x: T) -> T { x } |
| 多参数 | fn f<K, V>(k: K, v: V) {} |
| 带约束(行内) | fn f<T: Clone + std::fmt::Debug>(x: T) {} |
| 带约束(where) | fn f<T>(x: T) where T: Clone, T: std::fmt::Debug {} |
| 泛型结构体 | struct Wrapper<T> { inner: T } |
| 泛型结构体的 impl | impl<T: Clone> Wrapper<T> { ... } |
| 特化某个具体类型 | impl Wrapper<i32> { fn only_int(&self) {} } |
| 泛型枚举 | enum Either<L, R> { Left(L), Right(R) } |
| 关联类型 | trait It { type Item; fn next(&mut self) -> Option<Self::Item>; } |
| 关联常量 | trait HasMax { const MAX: u32; } |
| 默认类型参数 | struct A<T = i32> { v: T } |
| 生命周期 + 泛型 | fn f<'a, T: 'a>(x: &'a T) -> &'a T { x } |
| const 泛型(1.51) | struct Arr<const N: usize>([i32; N]); |
| 静态分发调用 | Arr::<3>([1, 2, 3]) |
rust
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Self { items: Vec::new() }
}
fn push(&mut self, v: T) {
self.items.push(v);
}
}
// 只在 T: Clone 时提供的方法:比给整个 impl 加约束更精细
impl<T: Clone> Stack<T> {
fn peek_cloned(&self) -> Option<T> {
self.items.last().cloned()
}
}
impl<T: std::fmt::Display> std::fmt::Display for Stack<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Stack(len={})", self.items.len())
}
}
fn main() {
let mut s = Stack::new();
s.push(1);
s.push(2);
println!("{} {:?}", s, s.peek_cloned()); // 输出:Stack(len=2) Some(2)
}🚀 进阶:
where子句在约束很长或涉及生命周期时比行内写法可读得多; 且只有where形式能表达高阶生命周期约束(HRTB),如where F: for<'a> Fn(&'a str) -> &'a str。
C.1.6 生命周期标注位置
| 位置 | 写法 | 说明 |
|---|---|---|
| 泛型参数列表 | fn f<'a>(x: &'a str) | 生命周期参数与类型参数并列声明 |
| 引用类型 | &'a T / &'a mut T | 标注在 & 之后 |
| 结构体字段 | struct S<'a> { s: &'a str } | 结构体借用外部数据时必须声明 |
| 结构体 impl | impl<'a> S<'a> { ... } | impl 也要重复声明 |
| 返回引用 | fn f<'a>(x: &'a str) -> &'a str | 输出生命周期必须是某个输入的子集 |
| 静态 | &'static str | 整个程序存活,字面量默认如此 |
| 省略(elision) | fn f(x: &str) -> &str | 单输入引用 ⇒ 输出自动同寿命 |
省略(&self) | fn m(&self) -> &str | 输出自动绑定到 &self 的生命周期 |
| 结构体方法返回内部引用 | fn get<'s>(&'s self) -> &'s str | &self 规则已覆盖,通常可省 |
| 多输入引用 | fn f<'a>(x: &'a str, y: &'a str) -> &'a str | 多输入时无法省略,必须显式 |
| trait 对象 | Box<dyn Trait + 'a> | 对象默认 'static,需缩短时显式写 |
| 静态约束 | T: 'static | 类型不含任何非 'static 借用 |
| 匿名占位 | Formatter<'_> | 让编译器推断,避免写 'static 撒谎 |
rust
// 唯一必须显式标注的情形:多个输入引用 + 一个输出引用
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
// 省略规则已足够,不用写生命周期
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// 结构体持有借用
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
// 返回 &str 绑定到 &self 的生命周期,而不是 'a
fn announce(&self, note: &str) -> &str {
println!("{note}");
self.text
}
}
fn main() {
let a = String::from("hello");
let e = Excerpt { text: &a };
println!("{} {}", longest("abc", "de"), first_word("hi there"));
println!("{}", e.announce("注意"));
}⚠️ 陷阱:生命周期描述的是引用之间的关系,不是「值活多久」。
'a应读作「这些引用的有效期至少有共同的重叠区间」。 若编译器说「borrowed value does not live long enough」,问题几乎总是 被借用的值先于引用被 drop,而不是「缺少一个'a」。
C.1.7 match 完整语法
| 形态 | 写法 | 说明 |
|---|---|---|
| 基本 | match v { 1 => "one", _ => "other" } | 必须穷尽(exhaustive) |
| 多值 | match v { 1 | 2 => "a", _ => "b" } | 或模式(or-pattern) |
| 范围 | match n { 0..=9 => "digit", _ => "other" } | 只能用于可比较的数值/字符 |
| 绑定 | match v { n @ 1..=9 => n, _ => 0 } | @ 既匹配又绑定 |
| 解构元组 | match t { (0, y) => y, (x, _) => x } | 逐位置匹配 |
| 解构结构体 | match p { Point { x: 0, y } => y, _ => 0 } | 可只用部分字段 |
| 解构枚举 | match s { Shape::Rect(w, h) => w * h, _ => 0.0 } | 覆盖变体 |
| 嵌套 | match o { Some(Ok(v)) => v, _ => 0 } | 层层嵌套 |
| guard | match n { x if x % 2 == 0 => "even", _ => "odd" } | if 附加条件,可引用绑定的名字 |
| 忽略剩余 | match p { Point { x, .. } => x } | .. 忽略其余字段 |
| 引用匹配 | match &opt { Some(s) => s.len(), None => 0 } | 注意匹配的是 &Option<T> |
| 默认 ergonomics | match opt { Some(s) => ..., None => ... } | &Option<T> 也能直接写 Some(s),s 为 &T |
| 守卫 + 或 | match c { 'a'..='z' | 'A'..='Z' if c != 'q' => 1, _ => 0 } | guard 作用于整个或模式 |
| 不可反驳模式 | let (a, b) = (1, 2); | let 只接受必然匹配的模式 |
| 可反驳模式 | let Some(x) = o; ❌ | let 不接受可反驳模式,改用 let ... else |
rust
#[derive(Debug)]
enum Cmd {
Quit,
Echo(String),
Move { x: i32, y: i32 },
}
fn run(c: Cmd) -> String {
match c {
Cmd::Quit => "bye".to_string(),
Cmd::Echo(s) if s.is_empty() => "(empty)".to_string(),
Cmd::Echo(s) => s,
Cmd::Move { x, y } if x == 0 && y == 0 => "原点".to_string(),
Cmd::Move { x, .. } if x < 0 => "负 x".to_string(),
// `n @ 0..=9` 同时绑定与匹配范围
Cmd::Move { y: n @ 0..=9, .. } => format!("近处 y={n}"),
Cmd::Move { y, .. } => format!("远处 y={y}"),
}
}
fn main() {
println!("{}", run(Cmd::Echo(String::new()))); // 输出:(empty)
println!("{}", run(Cmd::Move { x: -1, y: 5 })); // 输出:负 x
println!("{}", run(Cmd::Move { x: 1, y: 3 })); // 输出:近处 y=3
println!("{}", run(Cmd::Quit)); // 输出:bye
}⚠️ 陷阱:
match的匹配顺序是从上到下,第一个匹配上的分支胜出。 所以「带 guard 的宽泛模式」必须写在「不带 guard 的窄模式」前面, 否则后面的分支永远到不了(编译器对不可达模式会给unreachable_patterns警告)。 另外match x { 1 | 2 => .. }里的|是模式或,不是位或。
C.1.8 闭包三形态
| 形态 | trait 约束 | 能做什么 | 捕获方式 |
|---|---|---|---|
Fn | Fn(Args) -> Out | 可被多次调用,且不改变捕获状态 | 只读捕获(&T) |
FnMut | FnMut(Args) -> Out | 可被多次调用,可修改捕获状态 | 可变捕获(&mut T) |
FnOnce | FnOnce(Args) -> Out | 只保证可调用一次,可消费捕获的值 | 夺走所有权(T) |
层级关系:Fn: FnMut: FnOnce(能当 Fn 用的闭包一定也能当 FnMut/FnOnce 用)。 所以接收方应该尽量用最宽松的约束:FnOnce 最宽松,Fn 最严格。
| 写法 | 语法 |
|---|---|
| 无参 | || println!("hi") |
| 单参 | |x| x + 1(单表达式可省类型与花括号) |
| 多参 + 块 | |a: i32, b: i32| -> i32 { a + b } |
| 显式类型 | |x: i32| -> i32 { x * 2 } |
| 捕获不可变 | let f = || println!("{s}"); |
| 捕获可变 | let mut f = || count += 1;(f 需 mut) |
| 移动捕获 | move || println!("{s}")(常用于 thread::spawn / async) |
| 立即调用 | (|x| x * 2)(21) |
| 作为参数 | fn call<F: FnOnce() -> i32>(f: F) -> i32 { f() } |
| 从函数返回 | -> impl Fn(i32) -> i32 |
rust
fn main() {
let s = String::from("hello");
let by_ref = || s.len(); // Fn:只读捕获
println!("{}", by_ref()); // 可多次调用
let consume = move || s; // FnOnce:夺走 s
println!("{}", consume());
// println!("{s}"); // E0382:s 已被 move 进闭包
let mut count = 0;
let mut inc = || { count += 1; count }; // FnMut
println!("{} {}", inc(), inc()); // 输出:1 2
println!("{}", (|x: i32| x * 2)(21)); // 输出:42
}💡 对照:Java 的 lambda 只能捕获「事实不可变(effectively final)」的变量, Python 的闭包捕获变量本身(后期绑定,
lambda: i会拿到最终值),C++ 要显式写[&]/[=]。 Rust 的闭包默认按最小必要权限捕获(能借就不移),需要移走时你才写move。
C.1.9 异步函数骨架
rust
// Cargo.toml:tokio = { version = "1", features = ["full"] }
async fn fetch_len(url: &str) -> Result<usize, reqwest::Error> {
let body = reqwest::get(url).await?.text().await?;
Ok(body.len())
}
// 最小可运行骨架:需要一个运行期(runtime)来 poll future
#[tokio::main]
async fn main() {
let n = fetch_len("https://example.com").await.unwrap_or(0);
println!("{n}");
// 并发:join! 等待全部完成
let (a, b) = tokio::join!(fetch_len("https://example.com"), fetch_len("https://example.com"));
println!("{:?} {:?}", a.map(|x| x > 0), b.map(|x| x > 0));
}| 骨架 | 写法 | 备注 |
|---|---|---|
| 异步函数 | async fn f() -> T { .. } | 返回 impl Future<Output = T>,调用时不执行 |
| 异步块 | let fut = async { 1 + 1 }; | 可用于非函数上下文 |
| 等待 | x.await | 只能在 async 上下文里写 |
| 手动驱动 | futures::executor::block_on(fut) | 不引入 tokio 时的最简驱动 |
| 主函数 | #[tokio::main] async fn main() | 宏把 main 包进运行期 |
| 测试 | #[tokio::test] async fn t() {} | 异步测试,可用 #[tokio::test(flavor = "multi_thread")] |
| 生成任务 | tokio::spawn(async move { .. }) | 返回 JoinHandle,要求 'static + Send |
| 并发等待 | tokio::join!(a, b) | 全部完成,返回元组 |
| 竞速 | tokio::select! { .. } | 第一个就绪分支胜出,其余被 cancel |
| 异步 trait 方法(1.75) | trait T { async fn m(&self); } | 原生支持,不再需要 #[async_trait] |
| 异步 trait 的 dyn 安全 | -> Pin<Box<dyn Future<Output = ()> + Send + '_>> | 需要 dyn 时的显式返回类型 |
⚠️ 陷阱:
async fn返回的 future 是惰性的 —— 不.await就什么都不发生。 这与 JS 里不 await 也会开始执行(直到第一个 await)不同,也是「创建了却忘记 spawn」的静默 bug 来源。
🚀 进阶:
select!会取消未完成的分支(future 被 drop)。 若分支里有不可中断的副作用,需自己加重试或在 droppable guard 里处理。
C.1.10 宏定义骨架
| 类型 | 定义骨架 | 调用 | 备注 |
|---|---|---|---|
声明宏(macro_rules!) | macro_rules! name { ($x:expr) => { $x + 1 }; } | name!(5) | 按语法片段匹配,卫生(hygienic) |
| 导出宏 | #[macro_export] macro_rules! name { .. } | crate::name!() 或 name!() | 导出到 crate 根 |
| 作用域宏 | macro_rules! name { .. } + pub(crate) use name; | 本模块作用域内 | 1.30+ 的现代写法 |
| 重复匹配 | ($($x:expr),* $(,)?) => { vec![$($x),*] }; | v!(1, 2, 3) | $(..)* 表零次或多次,$(,)? 容忍尾逗号 |
| 片段类型 | $x:expr / :ty / :ident / :pat / :stmt / :block / :item / :literal / :tt / :path / :meta | — | 用 :tt 兜底最灵活 |
| 过程宏(derive) | #[proc_macro_derive(MyTrait)] pub fn d(input: TokenStream) -> TokenStream | #[derive(MyTrait)] | 必须写在独立的 proc-macro crate |
| 过程宏(属性) | #[proc_macro_attribute] pub fn attr(attr: TokenStream, item: TokenStream) -> TokenStream | #[attr] | 可改写被标注的项 |
| 过程宏(函数式) | #[proc_macro] pub fn m(input: TokenStream) -> TokenStream | m!(..) | 自由语法 |
toml
# 过程宏 crate 的 Cargo.toml 必须声明
[lib]
proc-macro = truerust
// 声明宏:实现一个简易的 hashmap! 字面量
macro_rules! my_map {
// $(..),* 匹配零或多个「k => v」,$(,)? 允许结尾多一个逗号
($($k:expr => $v:expr),* $(,)?) => {{
let mut m = std::collections::HashMap::new();
$( m.insert($k, $v); )*
m
}};
}
fn main() {
let m = my_map! { "a" => 1, "b" => 2, };
println!("{}", m["a"]); // 输出:1
}⚠️ 陷阱:宏展开发生在类型检查之前,所以宏里的错误信息往往指向展开后的代码。 调试用
cargo expand(见 C.9)看真实展开结果。
C.2 格式化输出速查
C.2.1 format! 语法全表
| 写法 | 作用 | 示例代码 | 结果 |
|---|---|---|---|
{} | 用 Display 格式化,按位置取参 | format!("{}", 1) | 1 |
{:?} | 用 Debug 格式化 | format!("{:?}", "a") | "a"(带引号) |
{:#?} | 美化版 Debug,多行缩进 | format!("{:#?}", vec![1]) | 多行 [\n 1,\n] |
{:>8} | 右对齐,总宽 8 | format!("{:>8}", "hi") | ␣␣␣␣␣␣hi |
{:<8} | 左对齐,总宽 8 | format!("{:<8}", "hi") | hi␣␣␣␣␣␣ |
{:^8} | 居中,总宽 8 | format!("{:^8}", "hi") | ␣␣␣hi␣␣␣ |
{:08.3} | 宽度 8、保留 3 位小数、补前导零 | format!("{:08.3}", 3.14159) | 0003.142 |
{:e} | 科学计数法(小写 e) | format!("{:e}", 1234.0) | 1.234e3 |
{:E} | 科学计数法(大写 E) | format!("{:E}", 1234.0) | 1.234E3 |
{:x} | 十六进制小写 | format!("{:x}", 255) | ff |
{:#x} | 带 0x 前缀的十六进制小写 | format!("{:#x}", 255) | 0xff |
{:X} | 十六进制大写 | format!("{:X}", 255) | FF |
{:#X} | 带 0x 前缀的大写十六进制 | format!("{:#X}", 255) | 0xFF |
{:b} | 二进制 | format!("{:b}", 5) | 101 |
{:#b} | 带 0b 前缀的二进制 | format!("{:#b}", 5) | 0b101 |
{:o} | 八进制 | format!("{:o}", 8) | 10 |
{:#o} | 带 0o 前缀的八进制 | format!("{:#o}", 8) | 0o10 |
{:p} | 指针地址 | format!("{:p}", &1) | 0x... |
{:.2} | 保留 2 位小数(四舍五入) | format!("{:.2}", 1.005) | 1.00(浮点表示所致) |
{:+.2} | 强制显示正负号 | format!("{:+.2}", 1.0) | +1.00 |
{name} | 捕获同名变量(1.58) | let name = "R"; format!("{name}") | R |
{name:>5} | 捕获变量 + 规格 | format!("{name:>5}") | ␣␣␣␣R |
{0} | 按索引取参 | format!("{0}-{0}", 7) | 7-7 |
{1} {0} | 按索引重排 | format!("{1} {0}", "a", "b") | b a |
{x} + 具名实参 | format!("{x}", x = 1) | 1 | |
{{}} | 转义花括号 | format!("{{}}") | {} |
{:.3e} | 科学计数 + 精度 | format!("{:.3e}", 1234.0) | 1.234e3 |
{:width$} | 宽度由具名参数提供 | format!("{:w$}", 1, w = 4) | ␣␣␣1 |
{:.*} | 精度由位置参数提供 | format!("{:.*}", 2, 1.2345) | 1.23 |
{:>8.2} | 组合:右对齐 + 精度 | format!("{:>8.2}", 1.5) | ␣␣␣␣␣1.50 |
rust
fn main() {
let name = "Rust";
let pi = 3.14159;
println!("{}", 42); // 输出:42
println!("{:?}", "hi"); // 输出:"hi"
println!("{:#?}", vec![1, 2]); // 输出:多行 [\n 1,\n 2,\n]
println!("[{:>8}]", "hi"); // 输出:[ hi]
println!("[{:<8}]", "hi"); // 输出:[hi ]
println!("[{:^8}]", "hi"); // 输出:[ hi ]
println!("{:08.3}", pi); // 输出:0003.142
println!("{:e} {:.2e}", 1234.0, 1234.0); // 输出:1.234e3 1.23e3
println!("{:x} {:#x} {:X}", 255, 255, 255); // 输出:ff 0xff FF
println!("{:b} {:#b} {:o}", 5, 5, 8); // 输出:101 0b101 10
println!("{name} 的宽度是 {:.1}", 1.25); // 输出:Rust 的宽度是 1.2
println!("{0} 和 {0}", 7); // 输出:7 和 7
println!("{{}} 是转义"); // 输出:{} 是转义
println!("{:>8.2}", 1.5); // 输出: 1.50
println!("{:p}", &pi); // 输出:0x...(每次运行不同)
}⚠️ 陷阱:
{:.2}用的是二进制浮点,1.005无法精确表示, 所以format!("{:.2}", 1.005)得到1.00而不是直觉上的1.01。 涉及金额请用rust_decimal或整数分表示。
🚀 进阶:
format_args!是format!/println!/write!的公共底层; 它返回借用了参数的Arguments,所以let a = format_args!("{x}");无法直接存进变量再返回。
C.2.2 输出宏族
| 宏 | 输出目标 | 末尾换行 | 返回值 |
|---|---|---|---|
print! | stdout | 否 | () |
println! | stdout | 是 | () |
eprint! | stderr | 否 | () |
eprintln! | stderr | 是 | () |
format! | 返回 String | — | String |
write! | 写入 impl Write/impl fmt::Write | 否 | io::Result<()> |
writeln! | 同上 | 是 | io::Result<()> |
dbg! | stderr(含文件名行号) | 是 | 返回传入的值 |
panic! | stderr + 展开 | — | ! |
assert! / assert_eq! / assert_ne! | panic(Debug 打印两侧) | — | () |
debug_assert! 系列 | 仅 debug 构建生效 | — | () |
todo! / unimplemented! / unreachable! | panic | — | ! |
rust
fn main() {
// dbg! 返回原值,因此可以内嵌在表达式里
let x = dbg!(1 + 2) * 2;
println!("{x}"); // 输出:6(stderr 上另有 [src/main.rs:3] 1 + 2 = 3)
assert_eq!(x, 6);
eprintln!("这行去 stderr");
}C.2.3 Display vs Debug vs LowerHex 的选用
| trait | 触发写法 | 面向谁 | derive 可用 | 何时实现 |
|---|---|---|---|---|
Display | {} | 最终用户 | ❌ 必须手写 | 类型有唯一「人类可读」表示时 |
Debug | {:?} / {:#?} | 开发者调试 | ✅ #[derive(Debug)] | 几乎所有类型都该有 |
LowerHex / UpperHex | {:x} / {:X} | 十六进制工具 | ❌ | 位掩码、哈希、协议字段 |
Binary / Octal | {:b} / {:o} | 位运算调试 | ❌ | 位标志集合 |
Pointer | {:p} | 地址观察 | ❌(引用已实现) | 极少自己实现 |
LowerExp / UpperExp | {:e} / {:E} | 科学计算 | ❌ | 数值类型 |
Debug(作为约束) | 泛型调试打印 | — | — | 用 T: Debug 而非 T: Display 做泛型约束 |
rust
use std::fmt;
// Color 也要能 Debug,否则包着它的 Pixel 无法 derive(Debug)
#[derive(Debug)]
struct Color {
r: u8,
g: u8,
b: u8,
}
// Debug 交给 derive,省事且不会写错
#[derive(Debug)]
struct Pixel(Color);
// Display 必须手写:表达「给用户看」的唯一形式
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// 用 write! 而不是 println!:Formatter 是格式化目标,不是终端
write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
}
}
// LowerHex:让 {:x} 输出三元组
impl fmt::LowerHex for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
}
fn main() {
let c = Color { r: 255, g: 128, b: 0 };
println!("{c}"); // 输出:#FF8000(Display)
println!("{:?}", Pixel(Color { r: 1, g: 2, b: 3 })); // 输出:Pixel(Color { r: 1, g: 2, b: 3 })
println!("{:x}", c); // 输出:ff8000(LowerHex)
}🧠 原理:格式化是编译期把每个
{}展开成一次fmt调用的过程, 编译器会静态检查「你用了{},那这个类型必须实现Display」。 这就是为什么拿Display当泛型约束会导致Vec<T>、HashMap之类无法打印 —— 它们只实现了Debug。泛型里请用T: Debug。
⚠️ 陷阱:
{:#?}的输出是多行的,混在单行日志里会很难读; 日志里建议用{:?}或引入tracing的结构化字段。
C.3 类型速查
C.3.1 标量类型位宽与范围
| 类型 | 位宽 | 范围 / 取值 | 默认/常用场景 |
|---|---|---|---|
i8 | 8 | −128 ~ 127 | 紧凑数组、字节级计算 |
i16 | 16 | −32 768 ~ 32 767 | 音频采样 |
i32 | 32 | −2 147 483 648 ~ 2 147 483 647 | 整数字面量默认类型 |
i64 | 64 | −9.22×10¹⁸ ~ 9.22×10¹⁸ | 一般计算、时间戳 |
i128 | 128 | ±1.70×10³⁸ | 大整数、UUID 运算 |
isize | 指针宽度(64 位平台为 64) | 同 i64(64 位平台) | 索引差值、len 相关运算 |
u8 | 8 | 0 ~ 255 | 字节(Vec<u8>、&[u8]) |
u16 | 16 | 0 ~ 65 535 | 端口号、UTF-16 码元 |
u32 | 32 | 0 ~ 4 294 967 295 | 计数、位掩码 |
u64 | 64 | 0 ~ 1.84×10¹⁹ | 哈希值、纳秒时间戳 |
u128 | 128 | 0 ~ 3.40×10³⁸ | 大整数、IPv6 数值化 |
usize | 指针宽度(64 位平台为 64) | 0 ~ 1.84×10¹⁹(64 位平台) | 索引与长度必须用它 |
f32 | 32 | ±1.18×10⁻³⁸ ~ ±3.40×10³⁸,7 位有效数字 | GPU、内存敏感场景 |
f64 | 64 | ±2.23×10⁻³⁰⁸ ~ ±1.80×10³⁰⁸,15 位有效数字 | 浮点字面量默认类型 |
char | 32 | 一个 Unicode 标量值(U+0000 ~ U+10FFFF,不含代理对) | 字符,4 字节 |
bool | 8 | true / false | 布尔 |
() | 0 | 唯一值 () | 「无返回值」的返回值 |
! | — | 无值(never type) | panic!、loop 无 break 的类型 |
整数字面量的便捷写法:
| 写法 | 含义 | 等价 |
|---|---|---|
1_000_000 | 下划线分隔 | 1000000 |
0xFF | 十六进制 | 255 |
0o77 | 八进制 | 63 |
0b1010 | 二进制 | 10 |
b'A' | 字节字面量 | 65u8 |
1u8 / 1_i64 | 后缀标注类型 | 显式类型 |
1.0f32 | 浮点后缀 | 1.0f32 |
⚠️ 陷阱:索引必须是
usize。v[0i32]会报E0277,需要v[0usize]或直接写v[0](字面量可推断)。 反过来,把usize当u32用(如fits u32的 FFI 字段)必须显式as转换。
🧠 原理:
i32/f64之所以是默认推断类型,是因为它们在 64 位平台上性能与体积平衡最好。 但溢出行为不同:i32等整数运算在 debug 构建下溢出会panic, 在 release 构建下静默回绕(wrapping)。想固定语义请用wrapping_add/checked_add/saturating_add/overflowing_add。
C.3.2 类型转换矩阵
| 手段 | 语法 | 适用场景 | 失败行为 | 会丢数据吗 |
|---|---|---|---|---|
as | x as u8 | 数值间截断/位重解释、指针转整数 | 不失败,静默截断 | 可能(高位丢弃) |
From | u32::from(x) / T::from(x) | 不会失败的放大转换(u8 → u32) | 编译期就不可能失败 | 不会 |
Into | let y: u32 = x.into(); | From 的反向写法,更易读 | 同上 | 不会 |
TryFrom | u8::try_from(x)? | 可能失败的缩小转换(u32 → u8) | 返回 Result | 不会(失败即 Err) |
TryInto | let y: u8 = x.try_into()?; | TryFrom 的反向写法 | 返回 Result | 不会 |
parse | "42".parse::<i32>()? | 字符串 → 数值/其他类型 | 返回 Result | 不适用 |
to_string / format! | x.to_string() | 任意 Display → String | 不失败 | 不适用 |
as_bytes / from_utf8 | s.as_bytes() / str::from_utf8(b)? | 字符串 ↔ 字节 | from_utf8 返回 Result | 不适用 |
unsafe 转换 | std::mem::transmute | 位级重解释(极危险) | 不失败,UB 风险自负 | 视类型而定 |
决策流程:
要把一个值转成另一种类型
├─ 目标是字符串? → to_string() / format!("{x}") / x.to_string()
├─ 来源是字符串?
│ ├─ 数值或实现了 FromStr → s.parse::<T>()? (返回 Result)
│ └─ 字节数组 → std::str::from_utf8(&b)? (校验 UTF-8)
├─ 都是数值?
│ ├─ 目标一定装得下(放大) → T::from(x) 或 x.into() (编译期保证安全)
│ └─ 可能装不下(缩小)
│ ├─ 想要显式检查 → T::try_from(x)? (推荐)
│ └─ 明确要截断/位模式 → x as T (写注释说明意图)
├─ 智能指针/包装类型? → as_ref() / as_mut() / into_inner()
└─ 以上都不适合? → 手写 impl From / TryFromrust
use std::convert::TryFrom;
fn main() {
// as:明确要截断
let big: u32 = 300;
println!("{}", big as u8); // 输出:44(300 % 256)
// From/Into:放大,绝不会失败
let small: u8 = 100;
let wide: u32 = u32::from(small);
let wide2: u32 = small.into();
println!("{wide} {wide2}"); // 输出:100 100
// TryFrom:缩小,显式处理失败
let ok = u8::try_from(200u32).unwrap();
let err = u8::try_from(300u32);
println!("{ok} {err:?}"); // 输出:200 Err(TryFromIntError(()))
// parse:字符串 → 数值,必须处理失败
let n: i32 = "42".parse().unwrap();
let bad = "4x".parse::<i32>();
println!("{n} {bad:?}"); // 输出:42 Err(ParseIntError { kind: InvalidDigit })
// 布尔与字符的转换
println!("{}", 'A' as u8); // 输出:65
println!("{}", 66u8 as char); // 输出:B
}
// 自定义 From:让转换融进 ? 与 into() 生态
struct Celsius(f64);
impl From<f64> for Celsius {
fn from(v: f64) -> Self {
Celsius(v)
}
}⚠️ 陷阱:
as用于浮点 → 整数时会向零截断且饱和(Rust 1.45 起):1e30_f64 as i32得到i32::MAX而不是未定义值。 但浮点精度丢失、NaN 转整数等语义仍需自己判断,优先用TryFrom。
🚀 进阶:
From有一个免费赠品 —— 实现了From<A> for B就自动获得Into<B> for A。 所以只需要实现From,不要手动写Into。 更进一步,?运算符会对错误值自动调用From::from,这是「错误类型自动上浮」的机制。
C.3.3 String 与 &str 互转全表
| 方向 | 写法 | 是否分配 | 备注 |
|---|---|---|---|
&str → String | String::from(s) | ✅ 分配 | 最明确的写法 |
&str → String | s.to_string() | ✅ | 来自 ToString(Display 的赠品) |
&str → String | s.to_owned() | ✅ | 语义上是「借用的自有版本」,Clone 风格 |
&str → String | s.into() | ✅ | 目标类型可由上下文推断时最简洁 |
String → &str | &s | ❌ 零拷贝 | 最常用,靠 Deref 自动转换 |
String → &str | s.as_str() | ❌ | 显式版,用在泛型/推断歧义处 |
String → &str | &s[..] | ❌ | 切片语法,可同时取子串 |
String → &[u8] | s.as_bytes() | ❌ | 直接看底层字节 |
String → Vec<u8> | s.into_bytes() | ❌ 转移 | 消费 String,零拷贝拿到缓冲区 |
&[u8] → &str | std::str::from_utf8(b)? | ❌ | 校验 UTF-8,返回 Result |
&[u8] → &str | unsafe { std::str::from_utf8_unchecked(b) } | ❌ | 跳过校验,需自行保证 |
Vec<u8> → String | String::from_utf8(v)? | ❌ 转移 | 校验 UTF-8 |
Vec<u8> → String | String::from_utf8_lossy(&v) | 可能 ✅ | 非法字节替换为 U+FFFD,返回 Cow<str> |
&[u8] → String | String::from_utf8_lossy(b).into_owned() | ✅ | 一定拿到自有 String |
char → String | c.to_string() | ✅ | |
String → char | s.chars().next() | ❌ | String 不能直接转 char |
String + &str | s + "x" | 可能 ✅ | + 消费左操作数(Add<&str> for String) |
| 拼接 | s.push_str("x") | 可能 ✅ | 原地追加,推荐 |
| 拼接多个 | format!("{a}{b}") | ✅ | 可读性最好 |
| 拼接切片集 | parts.join(",") | ✅ | Iterator<Item = &str> 或 &[String] |
rust
fn main() {
let owned: String = String::from("hello");
let borrowed: &str = &owned; // Deref 强转,零成本
let explicit: &str = owned.as_str();
println!("{borrowed} {explicit}"); // 输出:hello hello
// 三个 &str -> String 的写法完全等价,按语境挑
let a = "x".to_string();
let b = "x".to_owned();
let c: String = "x".into();
// 字节与字符串的边界:必须处理 UTF-8 校验
let bytes = "héllo".as_bytes();
let back = std::str::from_utf8(bytes).unwrap();
println!("{} {} {}", a, b, c); // 输出:x x x
println!("{back}"); // 输出:héllo
// 注意:len 是字节数,chars().count() 才是字符数
println!("{} {}", "héllo".len(), "héllo".chars().count()); // 输出:6 5
// 非 UTF-8 字节用 lossy 兜底
let lossy = String::from_utf8_lossy(&[0x66, 0x80, 0x6f]);
println!("{lossy}"); // 输出:f�o
}⚠️ 陷阱:
&s[0..3]是按字节切片,落在多字节字符中间会 panic(不是返回Err)。 想按字符安全切分,用s.char_indices()找边界,或直接用chars()迭代。
💡 对照:Java 的
String与StringBuilder是两种类型,Python 的str不可变、 拼接必须靠join。Rust 的String是可变且拥有所有权的,&str是借用的视图 —— 这正对应 C++ 的std::string与std::string_view(但 Rust 有借用检查器保证视图不悬垂)。
C.3.4 数字与字符串互转
| 目标 | 写法 | 失败类型 |
|---|---|---|
| 字符串 → 整数 | "42".parse::<i32>() | ParseIntError |
| 字符串 → 浮点 | "1.5".parse::<f64>() | ParseFloatError |
| 字符串 → 布尔 | "true".parse::<bool>() | ParseBoolError |
| 字符串 → 字符 | "a".parse::<char>() | ParseCharError |
| 字符串 → 带进制整数 | i64::from_str_radix("ff", 16) | ParseIntError |
| 字符串 → IP 地址 | "127.0.0.1".parse::<std::net::IpAddr>() | AddrParseError |
| 整数 → 字符串 | n.to_string() | — |
| 整数 → 指定进制 | format!("{n:x}") / format!("{n:b}") | — |
| 浮点 → 字符串 | format!("{x:.3}") | — |
| 字符串 → 自定义类型 | impl std::str::FromStr for T | 自定义 Err |
| 数值 → 字节(小端) | n.to_le_bytes() | — |
| 字节 → 数值 | i32::from_le_bytes(a) | 长度不匹配则编译错误 |
rust
use std::str::FromStr;
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
// 实现 FromStr 就能用 s.parse::<Point>() 和 s.parse()?(含 ? 的错误传播)
impl FromStr for Point {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (x, y) = s.split_once(',').unwrap_or((s, "0"));
Ok(Point {
x: x.trim().parse()?,
y: y.trim().parse()?,
})
}
}
fn main() {
println!("{}", "42".parse::<i32>().unwrap()); // 输出:42
println!("{}", i64::from_str_radix("ff", 16).unwrap()); // 输出:255
println!("{}", format!("{:x} {:b}", 255, 5)); // 输出:ff 101
println!("{:?}", "3,4".parse::<Point>().unwrap()); // 输出:Point { x: 3, y: 4 }
println!("{:?}", "3,4".parse::<Point>()); // 输出:Ok(Point { x: 3, y: 4 })
// 字节序转换是二进制协议的基本功
let b = 1u32.to_le_bytes();
println!("{:?} {}", b, u32::from_le_bytes(b)); // 输出:[1, 0, 0, 0] 1
}⚠️ 陷阱:
"1.5".parse::<i32>()会失败(InvalidDigit), 而" 42 "也会失败 ——parse不去除空白,需先.trim()。
C.4 集合操作速查
复杂度记法:O(1) 为摊还(amortized)均摊常数,O(log n) 为对数,O(n) 为线性。 哈希表复杂度为平均情况(最坏 O(n),受哈希质量与随机种子影响)。
C.4.1 Vec<T>
| 操作 | 方法 | 复杂度 | 备注 |
|---|---|---|---|
| 新建 | Vec::new() / vec![] | O(1) | Vec::with_capacity(n) 可预留避免扩容 |
| 尾部追加 | push(v) | 摊还 O(1) | 扩容时近似翻倍 |
| 尾部移除 | pop() | O(1) | 返回 Option<T> |
| 按下标读 | v[i] | O(1) | 越界 panic |
| 安全读 | v.get(i) | O(1) | 返回 Option<&T>,推荐用于外部输入 |
| 按下标改 | v[i] = x | O(1) | 需要 mut v |
| 中间插入 | insert(i, v) | O(n) | 后续元素整体后移 |
| 中间移除 | remove(i) | O(n) | 返回被移除的值,后续元素前移 |
| 交换移除 | swap_remove(i) | O(1) | 顺序被打乱,适合「不关心顺序」的集合 |
| 尾部批量追加 | extend(iter) / append(&mut other) | 摊还 O(k) | append 会清空另一个 Vec |
| 长度/容量 | len() / capacity() / is_empty() | O(1) | |
| 预留 | reserve(n) / with_capacity(n) | O(n) / O(1) | 已知规模时必做 |
| 收缩 | shrink_to_fit() | O(n) | 释放多余容量 |
| 清空 | clear() | O(n) | 保留容量 |
| 切片 | &v[1..3] → &[T] | O(1) | 越界 panic;get(1..3) 更安全 |
| 转数组切片 | as_slice() / as_mut_slice() | O(1) | |
| 遍历 | for x in &v / &mut v / v | O(n) | 三种所有权形态 |
| 查找 | contains(&x) | O(n) | 需要 T: PartialEq |
| 定位 | iter().position(pred) | O(n) | 返回 Option<usize> |
| 排序 | sort() / sort_by_key() | O(n log n) | 需要 T: Ord |
| 浮点排序 | sort_by(f64::total_cmp) | O(n log n) | f64 不实现 Ord,必须先 total_cmp 或 partial_cmp().unwrap() |
| 去重(需先排序) | dedup() | O(n) | 只合并相邻重复,先 sort() |
| 反转 | reverse() | O(n) | |
| 二分查找 | binary_search(&x) | O(log n) | 要求已排序,返回 Result<usize, usize> |
| 拆分 | split_off(i) | O(n) | 返回后半段的 Vec |
| 保留/删除 | retain(pred) | O(n) | 原地过滤 |
| 转换 | into_iter() / iter() / iter_mut() | O(1) | 消费 / 只读 / 可变 |
rust
fn main() {
let mut v: Vec<i32> = vec![3, 1, 2, 2];
v.push(4); // 尾部追加
v.sort(); // [1, 2, 2, 3, 4]
v.dedup(); // 相邻去重 → [1, 2, 3, 4]
v.insert(0, 0); // 中间插入 O(n)
let popped = v.pop(); // Some(4)
let safe = v.get(100); // None,不 panic
// swap_remove 是 O(1) 删除,代价是顺序变化
let mut u = vec![10, 20, 30];
let removed = u.swap_remove(0); // 返回 10,u 变成 [30, 20]
println!("{v:?} {popped:?} {safe:?} {removed} {u:?}");
// 输出:[0, 1, 2, 3] Some(4) None 10 [30, 20]
}⚠️ 陷阱:
for x in &v之后不能再v.push(..)—— 借用检查器会拒绝(E0502)。 需要边遍历边改时,先收集要改的索引,或改用retain/iter_mut/drain(..)。
C.4.2 VecDeque<T>
| 操作 | 方法 | 复杂度 | 备注 |
|---|---|---|---|
| 新建 | VecDeque::new() / with_capacity(n) | O(1) | 环形缓冲区 |
| 头部插入 | push_front(v) | 摊还 O(1) | Vec 做不到 |
| 尾部插入 | push_back(v) | 摊还 O(1) | 等价 Vec::push |
| 头部移除 | pop_front() | O(1) | 返回 Option<T> |
| 尾部移除 | pop_back() | O(1) | |
| 按下标读写 | d[i] / get(i) | O(1) | 逻辑索引,不是内存偏移 |
| 中间插入 | insert(i, v) | O(n) | |
| 中间移除 | remove(i) | O(n) | |
| 旋转 | rotate_left(k) / rotate_right(k) | O(n) | 常用于轮转队列 |
| 头尾访问 | front() / back() | O(1) | 返回 Option<&T> |
| 长度 | len() / is_empty() | O(1) | |
| 转连续切片 | make_contiguous() | O(n) | 返回 &mut [T],可能重排 |
| 拆分 | split_off(i) | O(n) | |
| 清空/保留 | clear() / retain(pred) | O(n) |
rust
use std::collections::VecDeque;
fn main() {
let mut q: VecDeque<i32> = VecDeque::new();
q.push_back(1);
q.push_back(2);
q.push_front(0); // O(1),Vec 做不到
println!("{:?}", q); // 输出:[0, 1, 2]
println!("{:?} {:?}", q.pop_front(), q.pop_back()); // 输出:Some(0) Some(2)
}💡 对照:Python 的
collections.deque、C++ 的std::deque、Java 的ArrayDeque。 需要 FIFO 队列 / 滑动窗口 / BFS 时,VecDeque是正确选择; 用Vec::remove(0)模拟出队会变成O(n)。
C.4.3 HashMap<K, V>
| 操作 | 方法 | 复杂度 | 备注 |
|---|---|---|---|
| 新建 | HashMap::new() / with_capacity(n) | O(1) | 默认哈希器 RandomState(抗哈希洪水) |
| 插入 | insert(k, v) | 平均 O(1) | 返回旧值 Option<V> |
| 读取 | get(&k) | 平均 O(1) | 返回 Option<&V>;map[&k] 会 panic |
| 可变读取 | get_mut(&k) | 平均 O(1) | 返回 Option<&mut V> |
| 删除 | remove(&k) | 平均 O(1) | 返回 Option<V> |
| 判断存在 | contains_key(&k) | 平均 O(1) | 再 get 会重复哈希,优先直接 get |
| 长度/清空 | len() / clear() | O(1) / O(n) | |
| 遍历 | for (k, v) in &map | O(n) | 顺序不保证 |
| 键集合 | keys() | O(n) | |
| 值集合 | values() / values_mut() | O(n) | |
| 条目 API | entry(k).or_insert(v) | 平均 O(1) | 一次哈希搞定「查或插」 |
| 条目 API | entry(k).or_insert_with(f) | 平均 O(1) | 惰性构造默认值 |
| 条目 API | entry(k).and_modify(f).or_insert(v) | 平均 O(1) | 有则改、无则插 |
| 计数惯用法 | *map.entry(k).or_insert(0) += 1 | 平均 O(1) | 词频统计标准写法 |
| 合并 | extend(iter) | O(k) | 后者覆盖前者 |
| 批量保留 | retain(|k, v| ..) | O(n) | |
| 取出并清空 | drain() | O(n) | |
| 取出所有权 | into_iter() | O(n) | |
| 自定义哈希 | HashMap<K, V, BuildHasherDefault<FxHasher>> | — | 非加密场景快得多(需 rustc-hash) |
rust
use std::collections::HashMap;
fn main() {
let mut word_count: HashMap<&str, u32> = HashMap::new();
for w in "the quick brown fox the lazy the".split_whitespace() {
// entry API:一次查找完成「不存在则插 0,然后加一」
*word_count.entry(w).or_insert(0) += 1;
}
// 按次数降序、次数相同按字典序 —— 顺序不保证的 HashMap 必须先收集再排
let mut ranked: Vec<(&str, u32)> = word_count.iter().map(|(k, v)| (*k, *v)).collect();
ranked.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
println!("{:?}", &ranked[..2]); // 输出:[("the", 3), ("brown", 1)]
// get_mut 原地修改
if let Some(n) = word_count.get_mut("fox") {
*n += 10;
}
println!("{:?}", word_count.get("fox")); // 输出:Some(11)
}⚠️ 陷阱:
HashMap的迭代顺序每次运行都可能不同(默认哈希器带随机种子)。 需要稳定输出必须先sort,或改用BTreeMap。 另外map[&k]在键不存在时会 panic,等价于map.get(&k).unwrap()。
🚀 进阶:安全关键场景可直接用默认哈希器(抗 HashDoS); 纯内部性能敏感场景可换
FxHashMap/AHashMap,通常有数倍提升。
C.4.4 HashSet<T>
| 操作 | 方法 | 复杂度 | 备注 |
|---|---|---|---|
| 新建 | HashSet::new() | O(1) | 内部就是 HashMap<T, ()> |
| 由迭代器构造 | HashSet::from([1, 2, 3]) / iter.collect() | O(n) | 自动去重 |
| 插入 | insert(v) | 平均 O(1) | 返回 bool:是否新插入(false 表示已存在) |
| 删除 | remove(&v) | 平均 O(1) | 返回 bool |
| 判断存在 | contains(&v) | 平均 O(1) | 去重判断的标准手段 |
| 长度 | len() / is_empty() | O(1) | |
| 遍历 | for x in &set | O(n) | 顺序不保证 |
| 并集 | a.union(&b) | O(n + m) | 生成 Iterator,可用 .collect() |
| 交集 | a.intersection(&b) | O(min(n, m)) | |
| 差集 | a.difference(&b) | O(n) | 在 a 不在 b |
| 对称差 | a.symmetric_difference(&b) | O(n + m) | 恰好一个集合含有 |
| 子集/超集 | is_subset(&b) / is_superset(&b) | O(n) | |
| 不相交 | is_disjoint(&b) | O(min(n, m)) | |
| 原地并/交/差 | extend / retain | O(k) / O(n) | 用 retain 手写交集最高效 |
| 清空/取出 | clear() / drain() | O(n) |
rust
use std::collections::HashSet;
fn main() {
let a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [3, 4].into_iter().collect();
println!("{:?}", a.intersection(&b).collect::<Vec<_>>()); // 输出:[3]
println!("{:?}", a.union(&b).count()); // 输出:4
println!("{:?}", a.difference(&b).count()); // 输出:2
// 去重:collect 到 HashSet 是最快写法(但丢失顺序)
let raw = vec![1, 1, 2, 3, 3];
let unique: HashSet<i32> = raw.iter().copied().collect();
println!("{}", unique.len()); // 输出:3
// 想保留首次出现顺序:HashSet 只用来记「见过没」
let mut seen = HashSet::new();
let ordered: Vec<i32> = raw.iter().copied().filter(|x| seen.insert(*x)).collect();
println!("{ordered:?}"); // 输出:[1, 2, 3]
}C.4.5 BTreeMap<K, V>
| 操作 | 方法 | 复杂度 | 备注 |
|---|---|---|---|
| 新建 | BTreeMap::new() | O(1) | B 树,键始终有序 |
| 插入/读取/删除 | insert / get / remove | O(log n) | 与 HashMap 同名同签名 |
| 条目 API | entry(k).or_insert(v) | O(log n) | 用法与 HashMap 一致 |
| 遍历 | for (k, v) in &map | O(n) | 按 key 升序,这是选它的主要理由 |
| 首个/末尾 | first_key_value() / last_key_value() | O(log n) | |
| 按范围取 | range(1..5) | O(log n + k) | 半开区间,也可用 ..= |
| 弹出最小/最大 | pop_first() / pop_last() | O(log n) | 用于优先队列式处理 |
| 相邻查找 | range(..=k).next_back() | O(log n) | 模拟「前驱」 |
| 长度/清空 | len() / clear() | O(1) / O(n) | |
| 键集合有序遍历 | keys() / values() / values_mut() | O(n) | 有序 |
| 合并 | extend(iter) | O(k log n) |
rust
use std::collections::BTreeMap;
fn main() {
let mut scores: BTreeMap<&str, i32> = BTreeMap::new();
scores.insert("carol", 90);
scores.insert("alice", 95);
scores.insert("bob", 80);
// 迭代顺序由 key 决定,与插入顺序无关
for (name, s) in &scores {
print!("{name}={s} ");
}
println!(); // 输出:alice=95 bob=80 carol=90
// 范围查询是 BTreeMap 的独门能力
let mid: Vec<&&str> = scores.range("b".."d").map(|(k, _)| k).collect();
println!("{mid:?}"); // 输出:["bob", "carol"]
println!("{:?}", scores.first_key_value()); // 输出:Some(("alice", &95))
}HashMap vs BTreeMap 选择:
| 需求 | 选谁 | 理由 |
|---|---|---|
| 只要快速查/插 | HashMap | 平均 O(1) 对 O(log n) |
| 需要按 key 有序输出 | BTreeMap | 免去自己排序 |
| 需要范围查询 / 前驱后继 | BTreeMap | 哈希表结构上做不到 |
键是浮点或无 Ord | HashMap | 只需 Hash + Eq |
| 输出必须可复现(测试、快照) | BTreeMap | 顺序确定 |
| 抗 HashDoS 最稳 | BTreeMap | 最坏也是 O(log n) |
C.4.6 String / &str 常用方法
| 操作 | 方法 | 复杂度 | 备注 |
|---|---|---|---|
| 新建空串 | String::new() | O(1) | |
| 预留容量 | String::with_capacity(n) | O(1) | 拼接前必做 |
追加 &str | push_str(s) | 摊还 O(k) | 原地 |
追加 char | push(c) | 摊还 O(1) | |
| 字节长度 | len() | O(1) | 不是字符数 |
| 字符数 | s.chars().count() | O(n) | |
| 判空 | is_empty() | O(1) | |
| 字符迭代 | s.chars() | O(n) | 按 Unicode 标量值 |
| 字节迭代 | s.bytes() | O(n) | 按 UTF-8 字节 |
| 索引+字符 | s.char_indices() | O(n) | 安全切片定位的必备工具 |
| 按行迭代 | s.lines() | O(n) | 自动去掉 \n 与 \r\n |
| 按空白切分 | s.split_whitespace() | O(n) | 连续空白算一个分隔符 |
| 按分隔符切分 | s.split(',') | O(n) | 保留空串 |
| 切一次 | s.split_once(':') | O(n) | 返回 Option<(&str, &str)> |
| 切分并收集 | s.split(',').collect::<Vec<_>>() | O(n) | |
| 拼接 | parts.join(",") | O(n) | 元素需 AsRef<str> |
| 去空白 | s.trim() / trim_start() / trim_end() | O(n) | 返回 &str |
| 前后缀判断 | s.starts_with(x) / ends_with(x) | O(k) | |
| 子串查找 | s.find(sub) / rfind(sub) | O(n·m) | 返回字节下标 Option<usize> |
| 包含 | s.contains(sub) / contains(char) | O(n) | |
| 替换 | s.replace(a, b) | O(n) | 返回新 String |
| 原地替换 | s.replace_range(1..3, "x") | O(n) | 需 mut |
| 大小写 | to_uppercase() / to_lowercase() | O(n) | 可能改变字节长度(如 ß) |
| 安全切片 | s.get(0..2) | O(1) | 返回 Option<&str>,不 panic |
| 危险切片 | &s[0..2] | O(1) | 落在字符中间会 panic |
| 转整数 | s.parse::<i32>() | O(n) | 返回 Result |
| 转字节 | s.as_bytes() | O(1) | 零拷贝 |
| 重复 | s.repeat(3) | O(n) | 返回新 String |
| 填充 | format!("{:>5}", s) | O(n) |
rust
fn main() {
let mut s = String::with_capacity(32); // 已知要拼几段就先预留
s.push_str("Hello");
s.push(',');
s.push(' ');
s.push_str("World");
println!("{} {}", s.len(), s.chars().count()); // 输出:12 12(ASCII 时两者相同)
println!("{:?}", s.split(", ").collect::<Vec<_>>()); // 输出:["Hello", "World"]
println!("{:?}", s.to_lowercase().split_whitespace().collect::<Vec<_>>());
// 输出:["hello,", "world"]
// 多字节字符:字节长度 ≠ 字符数
let zh = "中文abc";
println!("{} {}", zh.len(), zh.chars().count()); // 输出:9 5
// 安全切片:先找字符边界
let end = zh.char_indices().nth(2).map(|(i, _)| i).unwrap_or(zh.len());
println!("{}", &zh[..end]); // 输出:中文
println!("{:?}", zh.get(0..1)); // 输出:None(0..1 割裂了「中」)
}⚠️ 陷阱:
"中文".len()是6不是2。 所有基于下标的字符串操作(get、切片、find的返回值)都是字节下标。 涉及非 ASCII 时务必用char_indices()换算。
C.5 迭代器速查
C.5.1 适配器(adapter,惰性,返回新迭代器)
适配器不会立刻执行,只是在 Iterator 上叠一层包装;必须由消费器驱动。
| 适配器 | 签名/用途 | 备注 |
|---|---|---|
map | map(f) → Iterator<Item = U> | 一对一变换,最常用 |
filter | filter(pred) | 保留 pred(&item) == true 的项 |
filter_map | filter_map(f: FnMut(T) -> Option<U>) | 一步完成「映射 + 丢弃 None」 |
flat_map | flat_map(f: FnMut(T) -> IntoIterator) | 映射后展平一层 |
flatten | flatten() | 展平嵌套的 Iterator<Item: IntoIterator> |
enumerate | enumerate() → (usize, T) | 带下标 |
zip | zip(other) → (A, B) | 以短的一方为准截断 |
chain | chain(other) | 顺序拼接两个迭代器 |
rev | rev() | 反向,要求 DoubleEndedIterator |
take | take(n) | 只取前 n 个 |
take_while | take_while(pred) | 直到 pred 为 false(含首个 false 之后全丢) |
skip | skip(n) | 跳过前 n 个 |
skip_while | skip_while(pred) | 跳过开头满足条件的项 |
step_by | step_by(n) | 每 n 个取一个(n 不能为 0) |
peekable | peekable() → Peekable | 可 .peek() 看下一个而不消费 |
inspect | inspect(f) | 借用每项做副作用(日志),不改变数据 |
scan | scan(init, f) | 带状态的 map,f 返回 Option 可提前终止 |
cycle | cycle() | 无限重复(需配合 take) |
cloned / copied | cloned() / copied() | &T → T,copied 要求 T: Copy |
by_ref | it.by_ref().take(3) | 借用迭代器,之后还能继续用原迭代器 |
chunks(切片) | slice.chunks(3) | 按固定长度分块,最后一块可短 |
windows(切片) | slice.windows(2) | 滑动窗口,长度固定 |
split(切片) | slice.split(|x| *x == 0) | 按谓词切分 |
dedup_by(Vec) | v.dedup_by(|a, b| a.key == b.key) | 相邻去重(自定义判等) |
C.5.2 消费器(consumer,立即执行并产出结果)
| 消费器 | 签名/用途 | 复杂度 |
|---|---|---|
collect | collect::<Vec<_>>() / ::<HashMap<_,_>>() | O(n) |
collect(Result) | collect::<Result<Vec<_>, _>>() | O(n),遇 Err 短路 |
fold | fold(init, f) | O(n),万能归约 |
reduce | reduce(f) | O(n),无初值版本,返回 Option |
try_fold | try_fold(init, f) -> Result | O(n),可提前短路 |
sum / product | sum::<i32>() | O(n) |
count | count() | O(n)(ExactSizeIterator 可 O(1)) |
for_each | for_each(f) | O(n),代替 for 循环用在链尾 |
any / all | any(pred) / all(pred) | 短路 |
find / find_map | find(pred) → Option<T> | 短路 |
position | position(pred) → Option<usize> | 短路 |
max / min | max() / min() → Option<T> | O(n),需 Ord |
max_by_key / min_by_key | max_by_key(f) | O(n) |
max_by / min_by | max_by(cmp) | O(n),自定义比较 |
last | last() → Option<T> | O(n) |
nth | nth(2) → Option<T> | 消费掉前面所有项 |
take(n).collect() | 取前 n 个 | O(n) |
partition | partition::<Vec<_>, _>(pred) → (Vec, Vec) | O(n) |
unzip | unzip::<_, _, Vec<_>, Vec<_>>() | O(n),Vec<(A,B)> → (Vec<A>, Vec<B>) |
zip + for | for (a, b) in x.iter().zip(y) | O(n) |
all/any 混用注意 | any 空集返回 false,all 空集返回 true | 逻辑学上的空真 |
rust
use std::collections::HashMap;
fn main() {
let v = vec![5, 3, 8, 1, 9, 2];
// 适配器惰性:这一行没有任何计算发生
let lazy = v.iter().map(|x| x * 2).filter(|x| *x > 8);
// 消费器驱动:collect 才真正跑
let got: Vec<i32> = lazy.collect();
println!("{got:?}"); // 输出:[10, 16, 18]
let sum: i32 = v.iter().sum();
let max = v.iter().max();
let pos = v.iter().position(|x| *x == 8);
let (big, small): (Vec<i32>, Vec<i32>) = v.iter().partition(|x| **x >= 5);
println!("{sum} {max:?} {pos:?} {big:?} {small:?}");
// 输出:28 Some(9) Some(2) [5, 8, 9] [3, 1, 2]
// fold:任何「累加器」型需求都能表达
let desc = v.iter().fold(String::new(), |mut acc, x| {
acc.push_str(&x.to_string());
acc.push('-');
acc
});
println!("{desc}"); // 输出:5-3-8-1-9-2-
}C.5.3 链式配方(可直接抄)
配方 1 · 按 key 分组
rust
use std::collections::HashMap;
fn main() {
let words = ["apple", "avocado", "banana", "blueberry", "cherry"];
// entry API 是最省事的写法:一次哈希
let mut by_first: HashMap<char, Vec<&str>> = HashMap::new();
for w in words {
by_first.entry(w.chars().next().unwrap()).or_default().push(w);
}
let mut keys: Vec<char> = by_first.keys().copied().collect();
keys.sort();
for k in keys {
println!("{k}: {:?}", by_first[&k]);
}
// 输出:a: ["apple", "avocado"] / b: ["banana", "blueberry"] / c: ["cherry"]
}配方 2 · 保序去重
rust
use std::collections::HashSet;
fn main() {
let nums = vec![3, 1, 3, 2, 1, 4];
// HashSet::insert 返回「是否新插入」,正好当作 filter 的谓词
let mut seen = HashSet::new();
let unique: Vec<i32> = nums.iter().copied().filter(|x| seen.insert(*x)).collect();
println!("{unique:?}"); // 输出:[3, 1, 2, 4](保留首次出现顺序)
}配方 3 · 取最大 N 个(Top-N)
rust
use std::cmp::Reverse;
use std::collections::BinaryHeap;
fn main() {
let nums = vec![5, 1, 9, 3, 7, 2, 8];
// 小数据量:全排序最直观
let mut sorted = nums.clone();
sorted.sort_unstable_by(|a, b| b.cmp(a));
println!("{:?}", &sorted[..3]); // 输出:[9, 8, 7]
// 大数据流:用大小为 N 的最小堆,空间 O(N)、时间 O(n log N)
let n = 3;
let mut heap: BinaryHeap<Reverse<i32>> = BinaryHeap::new();
for x in nums {
heap.push(Reverse(x));
if heap.len() > n {
heap.pop(); // 弹掉当前最小的
}
}
// heap 里只剩「最大的 3 个」;此时再排序,代价只与 N 有关,而不是全量 n
let mut top: Vec<i32> = heap.into_iter().map(|r| r.0).collect();
top.sort_unstable_by(|a, b| b.cmp(a));
println!("{top:?}"); // 输出:[9, 8, 7]
// 注意:BinaryHeap 的迭代顺序是任意的,必须自己排一次才能得到有序结果
}配方 4 · 扁平化
rust
fn main() {
let nested = vec![vec![1, 2], vec![3], vec![4, 5, 6]];
// flatten:展平一层
let flat: Vec<i32> = nested.iter().flatten().copied().collect();
println!("{flat:?}"); // 输出:[1, 2, 3, 4, 5, 6]
// flat_map:映射 + 展平,适合「每项产生多个结果」
let words = ["hi there", "bye"];
let letters: Vec<char> = words.iter().flat_map(|s| s.chars()).collect();
println!("{}", letters.len()); // 输出:11(8 + 3,空格也计入)
}配方 5 · 转 HashMap / HashSet
rust
use std::collections::{HashMap, HashSet};
fn main() {
let pairs = vec![("a", 1), ("b", 2), ("c", 3)];
let map: HashMap<&str, i32> = pairs.iter().copied().collect();
println!("{:?}", map.get("b")); // 输出:Some(2)
// 由切片建索引表,值是引用(不复制数据)
let items = vec!["alpha", "beta", "gamma"];
let index: HashMap<&str, usize> = items.iter().enumerate().map(|(i, s)| (*s, i)).collect();
println!("{:?}", index.get("gamma")); // 输出:Some(2)
// 只想快速判断「存在与否」就用 HashSet
let vocab: HashSet<&str> = items.iter().copied().collect();
println!("{}", vocab.contains("beta")); // 输出:true
}配方 6 · 字符串切分统计(词频)
rust
use std::collections::HashMap;
fn main() {
let text = "the quick brown fox jumps over the lazy dog the end";
let mut freq: HashMap<&str, usize> = HashMap::new();
for w in text.split_whitespace() {
*freq.entry(w).or_insert(0) += 1;
}
// HashMap 无序,这里按 (次数降序, 词升序) 排出稳定结果
let mut counts: Vec<(&str, usize)> = freq.into_iter().collect();
counts.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
println!("{:?}", &counts[..3]); // 输出:[("the", 3), ("brown", 1), ("dog", 1)]
}配方 7 · zip 索引 / 双序列并行走
rust
fn main() {
let names = ["alice", "bob", "carol"];
let scores = [95, 80];
// zip 以短的一方为准,多余元素被静默丢弃
let paired: Vec<(&str, i32)> = names.iter().copied().zip(scores).collect();
println!("{paired:?}"); // 输出:[("alice", 95), ("bob", 80)]
// 索引:enumerate 比手写计数器更安全(不会越界)
for (i, name) in names.iter().enumerate() {
println!("{i}:{name}");
}
// 两个序列求点积
let a = [1, 2, 3];
let b = [4, 5, 6];
let dot: i32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
println!("{dot}"); // 输出:32
}配方 8 · 窗口 / 分块
rust
fn main() {
let data = [1, 2, 3, 4, 5, 6, 7];
// 滑动窗口:求相邻差
let diffs: Vec<i32> = data.windows(2).map(|w| w[1] - w[0]).collect();
println!("{diffs:?}"); // 输出:[1, 1, 1, 1, 1, 1]
// 固定分块:批量处理
let chunks: Vec<&[i32]> = data.chunks(3).collect();
println!("{:?}", chunks.iter().map(|c| c.len()).collect::<Vec<_>>()); // 输出:[3, 3, 1]
}配方 9 · 短路与聚合
rust
fn main() {
let nums: Vec<Option<i32>> = vec![Some(1), None, Some(3)];
// 只要有一个 None 就整体 None
let all: Option<Vec<i32>> = nums.iter().copied().collect();
println!("{all:?}"); // 输出:None
// 只取 Some 的部分
let some: Vec<i32> = nums.iter().filter_map(|x| *x).collect();
println!("{some:?}"); // 输出:[1, 3]
// 序列化式校验:任一失败即失败
let parsed: Result<Vec<i32>, _> = ["1", "2", "x"].iter().map(|s| s.parse::<i32>()).collect();
println!("{}", parsed.is_err()); // 输出:true
}⚠️ 陷阱:
iter()与into_iter()在Vec上产生不同Item:&Vec<T>的into_iter()也产生&T(因为IntoIterator for &Vec<T>), 而Vec<T>::into_iter()产生T(消费容器)。 在for x in &v里绑定的是&T,写成x + 1会因「不能把&i32当i32」而报错, 需要解引用*x或加.copied()。
🧠 原理:迭代器链之所以「零开销」,是因为编译器会把每一层
map/filter内联成单个循环,与手写for生成的机器码基本等价(〈工程化与常用 crate〉一章会实测)。 唯一的例外是collect()分配,以及dyn Iterator装箱。
C.6 Option / Result 组合子速查
C.6.1 Option<T> 组合子
| 组合子 | 签名/用途 | 备注 |
|---|---|---|
is_some / is_none | bool | 只判断,不取出 |
map | map(f: FnOnce(T) -> U) → Option<U> | 变换「有值」的情况 |
map_or | map_or(default, f) | 有值则 f,否则返回 default |
map_or_else | map_or_else(default_fn, f) | default 也惰性计算 |
and_then | and_then(f: FnOnce(T) -> Option<U>) | 链式组合,避免 Some(Some(x)) 嵌套 |
and | and(optb) | 有值则返回 optb,否则 None |
or | or(optb) | 无值则返回 optb |
or_else | or_else(f: FnOnce() -> Option<T>) | 惰性备选值 |
filter | filter(pred) | 值为 None 或条件不满足则丢弃 |
unwrap | 取出或 panic | 仅用于「不可能失败」处,且附 expect 理由 |
expect | 同上,panic 时带自定义信息 | 优于 unwrap |
unwrap_or | 取出或返回默认值 | 饥渴求值 |
unwrap_or_default | 取出或 T::default() | 需 T: Default |
unwrap_or_else | 惰性生成默认值 | 默认值昂贵时用 |
ok_or | ok_or(err) → Result<T, E> | 饥渴求值 |
ok_or_else | ok_or_else(f) → Result<T, E> | 惰性构造错误,推荐 |
transpose | Option<Result<T, E>> ↔ Result<Option<T>, E> | 两种「可能失败的可选值」互转 |
zip | zip(other) → Option<(A, B)> | 两者都有才有值 |
take | take() → 取出并留下 None | 需 &mut Option<T> |
replace | replace(v) → 旧值 | 需 &mut Option<T> |
get_or_insert | 为空则先插入 | 需 &mut Option<T> |
iter / into_iter | 把 Option 当 0/1 元素迭代器 | 便于接 flatten |
as_ref / as_mut | Option<&T> / Option<&mut T> | 不消费原 Option |
as_deref | Option<&T::Target> | Option<String> → Option<&str> |
rust
fn main() {
let a: Option<i32> = Some(3);
let b: Option<i32> = None;
println!("{:?}", a.map(|x| x * 2)); // 输出:Some(6)
println!("{:?}", b.map(|x| x * 2)); // 输出:None
println!("{}", b.unwrap_or_else(|| 0)); // 输出:0
println!("{:?}", b.or(Some(9))); // 输出:Some(9)
println!("{:?}", a.filter(|x| *x > 5)); // 输出:None
println!("{:?}", a.zip(Some("x"))); // 输出:Some((3, "x"))
// and_then 链:把「多个可能失败/为空的步骤」串起来
fn half(x: i32) -> Option<i32> {
if x % 2 == 0 { Some(x / 2) } else { None }
}
println!("{:?}", Some(8).and_then(half).and_then(half)); // 输出:Some(2)
println!("{:?}", Some(6).and_then(half).and_then(half)); // 输出:None
// as_deref:把 Option<String> 当 Option<&str> 用,避免 clone
let s = Some(String::from("hi"));
println!("{:?}", s.as_deref().map(str::len)); // 输出:Some(2)
}C.6.2 Result<T, E> 组合子
| 组合子 | 签名/用途 | 备注 |
|---|---|---|
is_ok / is_err | bool | 判断 |
map | map(f) → Result<U, E> | 只变换 Ok 值 |
map_err | map_err(f) → Result<T, F> | 只变换错误,错误类型转换主力 |
map_or / map_or_else | 带默认值 | |
and_then | and_then(f: FnOnce(T) -> Result<U, E>) | 链式组合,E 必须同为一种 |
or_else | or_else(f: FnOnce(E) -> Result<T, F>) | 错误恢复/降级 |
unwrap / expect | 取出或 panic | 原型阶段可用,库代码避免 |
unwrap_or / unwrap_or_default / unwrap_or_else | 兜底值 | |
ok | Result<T, E> → Option<T> | 丢弃错误 |
err | → Option<E> | 丢弃成功值 |
transpose | Option<Result<T,E>> ↔ Result<Option<T>,E> | |
collect(迭代器) | 把 Iterator<Item = Result<T, E>> 收成 Result<Vec<T>, E> | 遇首个 Err 短路 |
? | 失败即提前返回,并自动 From::from 转换错误 | 只能在返回 Result/Option 的函数里用 |
is_ok_and / is_err_and(1.70) | 带谓词的判断 | |
inspect / inspect_err(1.76) | 借用查看而不改变 | 调试与日志友好 |
as_ref / as_mut | Result<&T, &E> | 不消费 |
rust
use std::num::ParseIntError;
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
// ? 在 Err 时提前 return,并自动做 From 转换
let n: i32 = s.parse()?;
Ok(n * 2)
}
fn main() {
// map 只动 Ok,map_err 只动 Err
let ok: Result<i32, &str> = Ok(1);
let err: Result<i32, &str> = Err("bad");
println!("{:?} {:?}", ok.map(|x| x + 1), err.map(|x| x + 1));
println!("{:?}", err.map_err(|e| e.len())); // 输出:Some(2) Err("bad") Err(3)
// and_then 链
println!("{:?}", parse_and_double("21")); // 输出:Ok(42)
println!("{}", parse_and_double("x").is_err()); // 输出:true
// collect::<Result<_,_>> 用于「全成功才成功」
let all: Result<Vec<i32>, _> = ["1", "2", "3"].iter().map(|s| s.parse::<i32>()).collect();
println!("{all:?}"); // 输出:Ok([1, 2, 3])
}C.6.3 惯用配方
配方 · ok_or / ok_or_else:Option 升格为 Result
rust
fn main() {
let map = std::collections::HashMap::from([("a", 1)]);
// ok_or 会立即构造错误字符串(哪怕不需要),错误昂贵时用 ok_or_else
let cheap = map.get("a").ok_or("not found");
let lazy = map.get("b").ok_or_else(|| format!("key {} 不存在", "b"));
println!("{cheap:?} {lazy:?}"); // 输出:Ok(1) Err("key b 不存在")
}配方 · transpose:两种「嵌套可能性」互换
rust
fn main() {
// Option<Result<..>> → Result<Option<..>>:适合「可选字段解析失败要报错」
let a: Option<Result<i32, &str>> = Some(Ok(1));
let b: Option<Result<i32, &str>> = None;
println!("{:?} {:?}", a.transpose(), b.transpose());
// 输出:Ok(Some(1)) Ok(None)
// Result<Option<..>> → Option<Result<..>>:反向
let c: Result<Option<i32>, &str> = Err("boom");
println!("{:?}", c.transpose()); // 输出:Some(Err("boom"))
}配方 · collect::<Result<_, _>>():批量校验
rust
fn main() {
let rows = [("a", "1"), ("b", "2"), ("c", "nope")];
// 任意一行失败就整体失败,错误是第一个失败项
let parsed: Result<Vec<(&str, i32)>, _> = rows
.iter()
.map(|(k, v)| v.parse::<i32>().map(|n| (*k, n)))
.collect();
println!("{}", parsed.is_err()); // 输出:true
// 想收集全部错误(不短路)就先收集 Result 再分别处理
let results: Vec<Result<i32, _>> = rows.iter().map(|(_, v)| v.parse::<i32>()).collect();
let (oks, errs): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok);
println!("{} {}", oks.len(), errs.len()); // 输出:2 1
}配方 · ? 与自定义错误类型
rust
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Parse(ParseIntError),
Empty,
}
// 手写 From 让 ? 能自动把 ParseIntError 转成 AppError
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Parse(e) => write!(f, "解析失败:{e}"),
AppError::Empty => write!(f, "输入为空"),
}
}
}
// 让 AppError 成为真正的 std 错误,可用 Box<dyn Error> 承载
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Parse(e) => Some(e),
AppError::Empty => None,
}
}
}
fn run(s: &str) -> Result<i32, AppError> {
if s.trim().is_empty() {
return Err(AppError::Empty);
}
// 这里的 ? 会自动调用 From<ParseIntError> for AppError
let n: i32 = s.trim().parse()?;
Ok(n * 2)
}
fn main() {
println!("{:?}", run(" 21 ")); // 输出:Ok(42)
println!("{}", run("").unwrap_err()); // 输出:输入为空
}🚀 进阶:真实项目里手写
impl Error样板太多,惯例是 库代码用thiserror(cargo add thiserror)定义错误枚举, 应用代码用anyhow(cargo add anyhow)做anyhow::Result<T>与.context("..")。
⚠️ 陷阱:
unwrap()不是「错误处理」,它是「断言这里绝不会失败」。 一旦失败程序 panic 且无法恢复。生产代码里expect("已校验过非空,因此一定有首元素")至少留下了理由; 而?才是可恢复的默认选择。
C.7 智能指针与并发速查
C.7.1 智能指针一行式
| 类型 | 创建 | 常用方法 | 何时用 |
|---|---|---|---|
Box<T> | Box::new(v) | *b 解引用、Box::into_raw(少用) | 唯一所有权 + 堆分配;递归类型;Box<dyn Trait> |
Rc<T> | Rc::new(v) | Rc::clone(&r)、Rc::strong_count(&r)、Rc::get_mut、Rc::ptr_eq | 单线程共享只读所有权(图、树共享节点) |
Arc<T> | Arc::new(v) | Arc::clone(&a)、Arc::strong_count、Arc::ptr_eq | 多线程共享只读所有权;Send + Sync |
RefCell<T> | RefCell::new(v) | borrow()、borrow_mut()、try_borrow()、replace、into_inner | 单线程内部可变性,借用规则挪到运行期检查 |
Cell<T> | Cell::new(v) | get()(T: Copy)、set、replace、take | 单线程、T: Copy 或被 move 出的内部可变性 |
Mutex<T> | Mutex::new(v) | lock() → LockResult<MutexGuard>、try_lock、into_inner | 多线程互斥访问可变数据 |
RwLock<T> | RwLock::new(v) | read()、write()、try_read、try_write | 读多写少;读并行 |
AtomicUsize 等 | AtomicUsize::new(0) | load、store、fetch_add、compare_exchange、swap | 无锁计数/标志;需指定 Ordering |
OnceLock<T>(1.70) | OnceLock::new() | get、set、get_or_init | 全局懒初始化(替代 lazy_static) |
LazyLock<T>(1.80) | LazyLock::new(|| v) | 直接 *L 解引用 | 全局懒初始化且立即求值的语法糖 |
Cow<'a, str> | Cow::Borrowed("x") | to_mut()、into_owned() | 可能借用、可能拥有,避免无谓克隆 |
Weak<T> | Rc::downgrade(&r) | upgrade() → Option<Rc<T>> | 打破引用循环,防止内存泄漏 |
rust
use std::cell::{Cell, RefCell};
use std::rc::Rc;
fn main() {
// Box:把大对象放堆上,或让递归类型有确定大小
let boxed = Box::new(5);
println!("{} {}", boxed, *boxed); // 输出:5 5
// Rc:多所有者,引用计数
let shared = Rc::new(vec![1, 2, 3]);
let clone = Rc::clone(&shared); // 只增加计数,不复制数据
println!("{}", Rc::strong_count(&shared)); // 输出:2
drop(clone);
println!("{}", Rc::strong_count(&shared)); // 输出:1
// RefCell:编译期借用规则 → 运行期检查,越界即 panic
let cell = RefCell::new(10);
*cell.borrow_mut() += 5;
println!("{}", cell.borrow()); // 输出:15
// 同时持有可变与不可变借用会在运行期 panic(编译期不会报错)
// Cell:无需借用的读写,适合 Copy 类型
let counter = Cell::new(0);
counter.set(counter.get() + 1);
println!("{}", counter.get()); // 输出:1
// 组合成「共享 + 可变」:单线程版
let log = Rc::new(RefCell::new(Vec::new()));
log.borrow_mut().push("first");
println!("{:?}", log.borrow()); // 输出:["first"]
}选型决策:
需要共享所有权吗?
├─ 不需要 → Box<T>(或直接栈上,最优先)
└─ 需要
├─ 跨线程? ── 是 ─→ Arc<T>(要改就 Arc<Mutex<T>> / Arc<RwLock<T>>)
│ └ 简单计数/标志 ─→ Arc<AtomicUsize>
└─ 否 ─→ Rc<T>(要改就 Rc<RefCell<T>>)
└ 有环? ─→ 其中一条边用 Weak<T>⚠️ 陷阱:
Rc<T>/RefCell<T>不是Send/Sync, 跨线程使用会编译失败(这是保护,不是限制)。 多线程必须换Arc+Mutex/RwLock/Atomic*。 另外Rc的循环引用会永久泄漏,务必用Weak打断。
🧠 原理:
Box是零成本抽象(就是一次malloc),Rc/Arc每次clone都有原子(Arc)或非原子(Rc)计数增减,RefCell每次borrow都有运行期计数与分支。它们都不是免费的。
C.7.2 Arc<Mutex<T>> 共享计数器
rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc 提供跨线程的共享所有权,Mutex 提供可变访问
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..10 {
let c = Arc::clone(&counter); // 每个线程一份 Arc
// move 把 c 移进闭包;thread::spawn 要求 'static + Send
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// lock 返回 MutexGuard,离开作用域自动解锁(RAII)
// 若线程 panic,Mutex 会被「毒化」(poisoned),lock 返回 Err
let mut n = c.lock().unwrap();
*n += 1;
}
}));
}
for h in handles {
h.join().unwrap(); // 等所有线程结束
}
println!("{}", *counter.lock().unwrap()); // 输出:10000
}⚠️ 陷阱:
Mutex::lock()在锁被毒化时返回Err,所以unwrap()会让 「一个线程 panic」升级为「所有线程 panic」。库代码可考虑lock().unwrap_or_else(|e| e.into_inner())主动忽略毒化。
🚀 进阶:更细粒度可用
AtomicUsize(无锁、无需Mutex); 读多写少用RwLock;Mutex的保护范围应尽量小 —— 别在持锁时做 IO。
C.7.3 thread::spawn 与 thread::scope
rust
use std::thread;
fn main() {
// spawn:要求闭包 'static,借用外部数据必须先 move 所有权进去
let v = vec![1, 2, 3];
let h = thread::spawn(move || v.iter().sum::<i32>());
println!("{}", h.join().unwrap()); // 输出:6
// scope(1.63):可安全借用栈上数据,作用域结束时自动 join
let data = vec![1, 2, 3, 4];
let mut doubled = vec![0; data.len()];
thread::scope(|s| {
// 分块并行:借用 &data 与 &mut doubled 的不同片段,互不冲突
for (chunk_in, chunk_out) in data.chunks(2).zip(doubled.chunks_mut(2)) {
s.spawn(move || {
for (i, o) in chunk_in.iter().zip(chunk_out.iter_mut()) {
*o = i * 2;
}
});
}
}); // 所有 scoped 线程在此行之前 join 完毕
println!("{doubled:?}"); // 输出:[2, 4, 6, 8]
}| 对比 | thread::spawn | thread::scope |
|---|---|---|
| 闭包约束 | 'static(不能借用局部变量) | 可借用作用域内的局部变量 |
| 生命周期管理 | 手动 join(),忘记就「游离线程」 | 作用域结束自动 join |
| 返回值 | JoinHandle<T> | 无(结果写入借用的变量) |
| 适用 | 长驻后台任务 | 分治/并行计算(推荐) |
C.7.4 tokio::spawn / join! / select!
rust
// Cargo.toml: tokio = { version = "1", features = ["full"] }
use std::time::Duration;
use tokio::time::sleep;
async fn work(id: u32) -> u32 {
sleep(Duration::from_millis(10)).await;
id * 2
}
#[tokio::main]
async fn main() {
// join!:并发等待,全部完成才返回(不会取消任何一个)
let (a, b, c) = tokio::join!(work(1), work(2), work(3));
println!("{a} {b} {c}"); // 输出:2 4 6
// spawn:把任务交给运行期,返回 JoinHandle,要求 'static + Send
let handle = tokio::spawn(async move { work(4).await });
println!("{}", handle.await.unwrap()); // 输出:8
// select!:谁先就绪谁胜出,其余分支被取消(drop)
let fast = sleep(Duration::from_millis(5));
let slow = sleep(Duration::from_millis(100));
tokio::select! {
_ = fast => println!("fast 先完成"),
_ = slow => println!("slow 先完成"),
} // 输出:fast 先完成
}| 场景 | 写法 |
|---|---|
| 固定数量的并发 | tokio::join!(f1, f2, f3) |
| 动态数量的并发 | futures::future::join_all(v)(cargo add futures) |
| 动态数量的并发 + 结果容错 | futures::future::join_all(v).await 后逐项检查 |
| 只取最快结果 | tokio::select! |
| 带超时 | tokio::time::timeout(dur, fut).await -> Result<T, Elapsed> |
| 后台任务 | tokio::spawn(async move { .. }) |
| 限制并发数 | tokio::sync::Semaphore + Arc |
| 任务间通信 | tokio::sync::mpsc::channel(n) |
| 共享状态 | Arc<Mutex<T>>(短临界区)或 tokio::sync::Mutex(跨 .await 持锁) |
C.7.5 mpsc 通道
rust
use std::sync::mpsc;
use std::thread;
fn main() {
// 有界/无界:std 只提供无界 channel(内存可无限增长,注意背压)
let (tx, rx) = mpsc::channel::<i32>();
for i in 0..3 {
let tx = tx.clone(); // Sender 可克隆 = 多生产者
thread::spawn(move || {
tx.send(i * 10).unwrap();
}); // 原 tx 随线程结束被 drop
}
drop(tx); // 必须 drop 原 tx,否则下面循环永不结束
// 单消费者:Receiver 不能克隆
let mut got: Vec<i32> = rx.iter().collect();
got.sort();
println!("{got:?}"); // 输出:[0, 10, 20]
}| API | 说明 |
|---|---|
mpsc::channel() | 无界,send 不阻塞、永不失败(除非接收端已 drop) |
mpsc::sync_channel(n) | 有界,缓冲满时 send 阻塞(天然背压) |
tx.send(v) | 返回 Result<(), SendError<T>>,接收端消失即 Err |
rx.recv() | 阻塞直到有消息或所有 Sender 被 drop(返回 Err) |
rx.try_recv() | 非阻塞,返回 Result<T, TryRecvError> |
rx.iter() | 迭代到所有发送端关闭,用于 collect |
crossbeam-channel | 功能更强(多消费者、select),cargo add crossbeam-channel |
tokio::sync::mpsc | 异步版,send().await / recv().await |
⚠️ 陷阱:
for x in rx只有在所有制Sender都被 drop 后才结束。 忘记drop(tx)(或漏掉一个 clone 的tx)会导致循环永远阻塞。
C.7.6 原子类型
| 类型 | 常用方法 | 备注 |
|---|---|---|
AtomicBool | load / store / swap / compare_exchange | 标志位 |
AtomicUsize / AtomicIsize | fetch_add / fetch_sub | 计数器 |
AtomicU8 / AtomicU32 / AtomicU64 | 同上,另有 fetch_and / fetch_or / fetch_xor | 位标志集合 |
AtomicPtr<T> | load / store / compare_exchange | 无锁数据结构底层 |
Ordering 选择(记这三条即可覆盖绝大多数场景):
| Ordering | 语义 | 何时用 |
|---|---|---|
Relaxed | 只保证原子性,不保证顺序 | 纯计数(最终一致性不需要同步其它内存) |
Acquire / Release | 配对使用,建立「先行发生」关系 | 锁实现、发布-订阅标志位。store 用 Release,load 用 Acquire |
SeqCst | 全局单一总顺序,最强也最慢 | 不确定时的安全默认 |
rust
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let hits = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let h = Arc::clone(&hits);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// Relaxed 足够:我们只要计数正确,不需要用它同步别的内存
h.fetch_add(1, Ordering::Relaxed);
}
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", hits.load(Ordering::Relaxed)); // 输出:8000
}🚀 进阶:
Ordering是最容易写错的地方。若无法确信Relaxed/Acquire-Release的正确性,先用SeqCst,性能瓶颈确认后再优化。 深入请读《Rust Atomics and Locks》(见 附录 D)。
C.8 文件与 IO 速查
C.8.1 std::fs 常用函数
| 操作 | 写法 | 备注 |
|---|---|---|
读整个文件为 String | std::fs::read_to_string("a.txt")? | 返回 io::Result<String>,会校验 UTF-8 |
| 读整个文件为字节 | std::fs::read("a.bin")? | 返回 io::Result<Vec<u8>> |
| 一次写整个文件 | std::fs::write("a.txt", "hi")? | 覆盖写,自动创建/截断 |
| 追加写 | 见下方 OpenOptions | fs::write 不能追加 |
| 打开文件 | File::open("a.txt")? | 只读 |
| 创建文件 | File::create("a.txt")? | 创建或截断为 0 字节 |
| 创建目录 | std::fs::create_dir("d")? | 父目录不存在会失败 |
| 递归创建目录 | std::fs::create_dir_all("a/b/c")? | 推荐,幂等 |
| 删除文件 | std::fs::remove_file("a.txt")? | |
| 删除空目录 | std::fs::remove_dir("d")? | 非空会失败 |
| 递归删除目录 | std::fs::remove_dir_all("d")? | ⚠️ 不可恢复 |
| 重命名 / 移动 | std::fs::rename("a", "b")? | 同盘内是移动 |
| 复制 | std::fs::copy("a", "b")? | 返回复制的字节数 |
| 读取目录项 | std::fs::read_dir(".")? | 返回 io::Result<ReadDir>,项是 Result<DirEntry> |
| 元数据 | std::fs::metadata("a.txt")? | len()、is_file()、is_dir()、modified() |
| 判断存在 | Path::new("a.txt").exists() | 注意 TOCTOU:检查后再打开可能已被删除 |
| 规范化路径 | std::fs::canonicalize("..")? | 解析 ../符号链接,Windows 上返回 \\?\ 前缀 |
| 硬链接 / 符号链接 | std::fs::hard_link / symlink | Windows 上 symlink 需权限 |
rust
use std::fs;
use std::io;
fn main() -> io::Result<()> {
let dir = std::env::temp_dir().join("rust_quickref_demo");
fs::create_dir_all(&dir)?; // 幂等,父目录自动补齐
let file = dir.join("demo.txt");
fs::write(&file, "first line\nsecond line\n")?;
// 一次读全(小文件首选)
let content = fs::read_to_string(&file)?;
println!("{}", content.lines().count()); // 输出:2
// 遍历目录
let mut names: Vec<String> = fs::read_dir(&dir)?
.filter_map(|e| e.ok()) // DirEntry 本身也是 Result
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
names.sort();
println!("{names:?}"); // 输出:["demo.txt"]
// 清理(示例用;生产代码删目录前务必确认路径)
fs::remove_dir_all(&dir)?;
Ok(()) // main 返回 Result 时,Err 会以非零码退出
}⚠️ 陷阱:
fs::read_to_string遇到非 UTF-8 内容会返回InvalidData错误。读二进制、图片、压缩包请用fs::read。
C.8.2 Path / PathBuf
| 操作 | 写法 | 备注 |
|---|---|---|
| 构造 | Path::new("a/b.txt") / PathBuf::from("a/b.txt") | Path 是借用视图,PathBuf 是拥有者 |
| 拼接 | base.join("sub/file.txt") | 若参数是绝对路径则替换整个路径 |
| 取扩展名 | p.extension() → Option<&OsStr> | 需 to_str() 转 &str |
| 去扩展名 | p.with_extension("md") | 返回新 PathBuf |
| 取文件名 | p.file_name() → Option<&OsStr> | 带扩展名 |
| 取文件主干 | p.file_stem() | 不带扩展名 |
| 取父目录 | p.parent() → Option<&Path> | 相对单段路径可能为 None/空 |
| 是否绝对 | p.is_absolute() / is_relative() | Windows 上 C:\ 与 \\srv\ 都算绝对 |
| 是否存在 | p.exists() / is_file() / is_dir() | 有 IO 开销 |
| 显示用字符串 | p.display() | 唯一可靠的打印方式(OsStr 未必是 UTF-8) |
转 &str | p.to_str() → Option<&str> | 非 UTF-8 时是 None |
| 损失式转换 | p.to_string_lossy() → Cow<str> | 非法字节替换为 U+FFFD |
| 遍历组件 | p.components() | 平台无关地逐段处理 |
| 由字符串解析 | PathBuf::from(s) / p.push("x") | push 就地修改 |
转为 String | p.into_os_string().into_string() | 失败时拿回原 OsString |
rust
use std::path::{Path, PathBuf};
fn main() {
let p = Path::new(r"C:\Users\demo\notes\ch1.md");
println!("{:?}", p.file_name()); // 输出:Some("ch1.md")
println!("{:?}", p.file_stem()); // 输出:Some("ch1")
println!("{:?}", p.extension()); // 输出:Some("md")
println!("{:?}", p.parent().map(|x| x.display().to_string()));
// 输出:Some("C:\\Users\\demo\\notes")
// join 与 with_extension 是构建路径的正确方式,不要手拼字符串
let out: PathBuf = p.parent().unwrap().join("ch2.md");
println!("{}", out.display()); // 输出:C:\Users\demo\notes\ch2.md
println!("{}", p.with_extension("txt").display()); // 输出:C:\Users\demo\notes\ch1.txt
// 注意 join 的替换语义:参数是绝对路径时前面的路径被丢弃
println!("{}", Path::new("a/b").join("C:/x").display()); // 输出:C:/x
// 跨平台建议:用 components() 而不是 split('/')
let n = Path::new("a/b/c.txt").components().count();
println!("{n}"); // 输出:3
}⚠️ 陷阱:不要把路径当字符串拼接(
format!("{}/{}", dir, file))—— Windows 用\、Unix 用/,且重复分隔符与..不会归一。 永远用PathBuf::join。
C.8.3 缓冲读写与 lines()
rust
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
fn main() -> io::Result<()> {
let path = std::env::temp_dir().join("rust_quickref_lines.txt");
std::fs::write(&path, "alpha\nbeta\ngamma\n")?;
// BufReader:包一层缓冲区,避免每次 read 都陷入系统调用
let file = File::open(&path)?;
let reader = BufReader::new(file);
// lines() 会剥离 \n 与 \r\n(Windows 换行不会留下 \r)
let mut lines: Vec<String> = Vec::new();
for line in reader.lines() {
lines.push(line?); // 每行都是 io::Result<String>
}
println!("{lines:?}"); // 输出:["alpha", "beta", "gamma"]
// BufWriter:必须显式 flush,否则缓冲区内容可能丢在 drop 之前不被检查
let out = File::create(&path)?;
let mut w = BufWriter::new(out);
for (i, l) in lines.iter().enumerate() {
writeln!(w, "{i}:{l}")?; // writeln! 需要 use std::io::Write
}
w.flush()?; // 关键:把缓冲刷到磁盘
let back = std::fs::read_to_string(&path)?;
println!("{back}"); // 输出:0:alpha\n1:beta\n2:gamma\n
std::fs::remove_file(&path)?;
Ok(())
}| 常用 | 说明 |
|---|---|
BufReader::new(f) | 默认 8 KiB 缓冲 |
BufReader::with_capacity(n, f) | 自定义缓冲大小 |
.lines() | 按行迭代,返回 io::Result<String> |
.read_line(&mut s) | 追加一行到已有 String(包含换行符) |
.read_to_string(&mut s) | 读到 EOF |
.read_until(b'\n', &mut v) | 按字节读 |
.bytes() | 逐字节迭代,返回 io::Result<u8> |
BufWriter::new(f) | 写缓冲,减少系统调用 |
writeln!(w, "..") | 写一行,需 use std::io::Write |
w.flush() | 手动刷盘;BufWriter 在 drop 时的错误会被忽略 |
stdin().lines() | 读标准输入,交互脚本常用 |
reader.split(b',') | 按字节分隔符切分 |
⚠️ 陷阱:
BufWriter被 drop 时会尝试 flush,但错误会被静默丢弃。 关键写入(数据库文件、配置)必须显式flush()?,否则可能既没报错也没落盘。
C.8.4 std::process::Command
rust
use std::process::Command;
fn main() -> std::io::Result<()> {
// 捕获输出:不捕获的话子进程直接继承父进程的 stdout/stderr
let out = Command::new("rustc").arg("--version").output()?;
println!("{}", out.status.success()); // 输出:true
// stdout 是 Vec<u8>,需要自己转字符串(子进程输出未必是 UTF-8)
print!("{}", String::from_utf8_lossy(&out.stdout)); // 输出:rustc 1.98.1 ...
// 想控制 stdin / 环境变量 / 工作目录
let st = Command::new("cmd")
.args(["/C", "echo", "hi"]) // Windows 上必须经 cmd /C
.current_dir(std::env::temp_dir())
.env("MY_VAR", "1")
.status()?; // 继承 stdio,返回 ExitStatus
println!("{}", st.code().unwrap_or(-1)); // 输出:0
// 文本模式下想拿到退出码而不是 panic,用 status() 自己判断
let failed = Command::new("cmd").args(["/C", "exit", "3"]).status()?;
println!("{}", failed.code().unwrap_or(-1)); // 输出:3
Ok(())
}| API | 说明 |
|---|---|
Command::new(prog) | 不经 shell,直接 exec;找不到程序返回 Err |
.arg(x) / .args([..]) | 逐个追加参数 |
.current_dir(p) | 设置工作目录 |
.env(k, v) / .envs(map) / .env_remove(k) | 环境变量 |
.stdin(Stdio::piped()) | 接管子进程标准输入 |
.stdout(Stdio::piped()) / .stderr(Stdio::null()) | 接管或丢弃输出 |
.output() | 等它结束并捕获 stdout/stderr |
.status() | 等它结束,stdio 继承 |
.spawn() | 不等待,返回 Child(child.wait() / kill()) |
ExitStatus::success() / .code() | 退出状态;被信号杀死时 code() 为 None |
⚠️ 陷阱:
Command::new不经过 shell,所以Command::new("echo hi")会失败 (没这个可执行文件),管道、通配符、&&也都不生效。 需要 shell 语义时显式调用cmd /C(Windows)或sh -c(Unix), 并且绝不要把用户输入直接拼进 shell 命令字符串 —— 那是命令注入。
🚀 进阶:想要更友好的进程管理可用
duct或xshell; 想要类型化的 C 库调用看bindgen(见 附录 D)。
C.8.5 环境变量与命令行参数
| 操作 | 写法 | 备注 |
|---|---|---|
| 全部参数 | std::env::args() | 返回 String(非 UTF-8 参数会 panic) |
| 全部参数(容错) | std::env::args_os() | 返回 OsString,稳健选择 |
| 程序名 | args().next() | 第一个元素是 argv[0] |
| 读环境变量 | std::env::var("PATH") | 返回 Result<String, VarError> |
| 读环境变量(带默认) | env::var("X").unwrap_or_else(|_| "default".into()) | 常用惯用法 |
| 读任意字节变量 | env::var_os("PATH") | 返回 Option<OsString> |
| 设置变量 | std::env::set_var("K", "v") | 2024 edition:是 unsafe fn(多线程下不安全) |
| 删除变量 | std::env::remove_var("K") | 同上,unsafe |
| 全部变量 | std::env::vars() | 遍历 (String, String) |
| 当前目录 | env::current_dir() / set_current_dir(p) | 返回 io::Result<PathBuf> |
| 临时目录 | env::temp_dir() | 跨平台 |
| 退出码 | std::process::exit(1) | 不运行析构,慎用 |
无第三方依赖的简易参数解析:
rust
use std::env;
fn main() {
// 先 collect 成 Vec,便于按下标访问与末尾判断
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
// 惯例:用法信息写 stderr,退出码非零
eprintln!("用法:{} <名字> [--upper]", args[0]);
std::process::exit(2);
}
let name = &args[1];
let upper = args.iter().any(|a| a == "--upper"); // 不加 -- 视为位置参数
let out = if upper { name.to_uppercase() } else { name.clone() };
println!("你好, {out}!"); // 输出:你好, <名字>!
// 环境变量:带默认值
let level = env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string());
println!("{level}"); // 输出:info(未设置时)
}⚠️ 陷阱:
env::args()在第 0 个位置放的是程序路径,不是业务参数。 另外 2024 edition 起set_var/remove_var是unsafe函数: 在有多线程时修改变量会与getenv竞态,能用配置结构体就别用环境变量。 真实 CLI 建议用clap(cargo add clap -F derive)。
C.9 Cargo 命令与配置速查
C.9.1 子命令
| 命令 | 作用 | 常用 flag |
|---|---|---|
cargo new 名字 | 新建包(默认含 git 与 src/main.rs) | --lib 建库、--vcs none 不建 git、--edition 2024 |
cargo init | 在现有目录里初始化包 | --lib、--name x |
cargo add serde | 添加依赖并写入 Cargo.toml | -F derive(features)、--dev(开发依赖)、--optional |
cargo remove serde | 移除依赖 | --dev |
cargo run | 编译并运行(默认 bin) | --bin x、--release、-- 参数 传给程序 |
cargo build | 编译,不运行 | --release、--target 三元组、--all-targets、-j N |
cargo check | 只做类型检查,不生成机器码 | 最快的反馈循环,写代码时首选 |
cargo test | 编译并运行测试 | -- --nocapture 显示输出、--lib、--doc、--test 名、--no-run |
cargo bench | 运行基准测试 | 需 #[bench](nightly)或 criterion |
cargo doc | 生成文档 | --open 打开浏览器、--no-deps、--document-private-items |
cargo fmt | 格式化代码 | --check 只检查(CI 用)、--all、-- --edition 2024 |
cargo clippy | lint 检查 | --all-targets --all-features -- -D warnings(CI 惯用) |
cargo update | 更新 Cargo.lock 到最新兼容版本 | -p crate 只更新一个、--precise 1.2.3 |
cargo tree | 打印依赖树 | -d 只显示重复依赖、-i crate 反向查谁依赖它、-e features |
cargo expand | 展开宏后的真实代码 | 需 cargo install cargo-expand;--bin x |
cargo install crate | 从 crates.io 安装二进制 | --locked、--git URL、--path .、--force |
cargo uninstall crate | 卸载已安装的二进制 | |
cargo publish | 发布到 crates.io | --dry-run 先演练、--allow-dirty |
cargo package | 打包 .crate 文件(不发布) | --list 查看将被打包的文件 |
cargo fix | 自动应用编译器建议 | --edition(跨 edition 迁移)、--allow-dirty、--broken-code |
cargo metadata | 输出工程元数据的 JSON | --format-version 1,供工具链消费 |
cargo clean | 删除 target/ | -p crate 只清一个包的产物 |
cargo vendor | 把依赖源码拷到本地 | 离线构建/审计用 |
cargo rustc | 向 rustc 传额外参数 | -- -Zunpretty=expanded(nightly) |
cargo login / cargo logout | 管理 crates.io token | token 存在 ~/.cargo/credentials.toml |
cargo search 关键词 | 搜索 crates.io | --limit 5 |
cargo info crate | 查看 crate 元信息 | 较新版本提供 |
写代码时的黄金循环:
powershell
cargo check # 秒级反馈,改一行就重跑
cargo clippy --all-targets -- -D warnings # 提交前
cargo fmt # 提交前
cargo test # 提交前
cargo build --release # 需要真实性能时中间产物与工具:
| 工具 | 安装 | 作用 |
|---|---|---|
cargo-nextest | cargo install cargo-nextest | 更快的测试运行器,cargo nextest run |
cargo-expand | cargo install cargo-expand | 展开宏,cargo expand |
cargo-llvm-cov | cargo install cargo-llvm-cov | 覆盖率,cargo llvm-cov --html |
cargo-flamegraph | cargo install cargo-flamegraph | 生成火焰图,cargo flamegraph |
cargo-audit | cargo install cargo-audit | 检查依赖的已知漏洞,cargo audit |
cargo-deny | cargo install cargo-deny | 依赖许可证/来源/重复版本策略 |
cargo-watch / bacon | cargo install bacon | 文件变化自动重跑检查 |
cargo-miri | rustup component add miri(nightly) | 检测 UB,cargo miri test |
C.9.2 Cargo.toml 字段速查
toml
# ── 包元数据 ────────────────────────────────────────────────
[package]
name = "my-app" # crate 名;二进制产物名同此
version = "0.1.0" # 语义化版本 MAJOR.MINOR.PATCH
edition = "2024" # 语言版本;1.98.1 支持 2015/2018/2021/2024
rust-version = "1.85" # 最低支持的 rustc(MSRV),会被 resolver 校验
authors = ["You <you@example.com>"]
description = "一句话说明" # 发布到 crates.io 时必填
license = "MIT OR Apache-2.0" # 或 license-file = "LICENSE"
repository = "https://github.com/you/repo"
homepage = "https://example.com"
documentation = "https://docs.rs/my-app"
readme = "README.md"
keywords = ["cli", "parser"] # 最多 5 个,每个 ≤ 20 字符
categories = ["command-line-utilities"]
exclude = ["tests/fixtures/**"] # 不打包进 .crate
include = ["src/**", "Cargo.toml"]
publish = false # true/false 或允许发布的 registry 列表
default-run = "my-app" # 多个 bin 时 cargo run 默认跑哪个
build = "build.rs" # 自定义构建脚本路径
# ── 依赖 ───────────────────────────────────────────────────
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
anyhow = "1"
thiserror = "2"
regex = "1"
# 路径依赖(同一仓库内的其他 crate)
my-core = { path = "../my-core" }
# Git 依赖(可指定分支/标签/提交)
my-lib = { git = "https://github.com/you/lib", branch = "main" }
# 可选依赖:只有开启 feature 时才编译
tracing = { version = "0.1", optional = true }
[dev-dependencies] # 仅测试/示例/基准使用,不进最终产物
criterion = "0.5"
proptest = "1"
[build-dependencies] # build.rs 专用
cc = "1"
[target.'cfg(windows)'.dependencies] # 平台条件依赖
winapi = "0.3"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
# ── 特性开关 ───────────────────────────────────────────────
[features]
default = ["std"]
std = []
# feature 可以启用其他 feature 或可选依赖
full-logging = ["tracing", "std"]
# ── 产物目标 ───────────────────────────────────────────────
[lib]
name = "my_lib" # 库名(自动把 - 转成 _)
crate-type = ["rlib"] # cdylib / staticlib / proc-macro 等
path = "src/lib.rs"
bench = false
doctest = true
[[bin]]
name = "my-app"
path = "src/main.rs"
[[example]]
name = "demo"
path = "examples/demo.rs"
[[test]]
name = "integration"
path = "tests/integration.rs"
[[bench]]
name = "speed"
harness = false # criterion 要求关掉内置 harness
# ── 构建配置 ───────────────────────────────────────────────
[profile.dev]
opt-level = 0 # 0=不优化(编译快),3=最大优化
debug = true # 是否生成调试信息
overflow-checks = true # 整数溢出 panic(dev 默认 true)
panic = "unwind" # unwind(可 catch)或 abort(体积小)
[profile.release]
opt-level = 3
lto = "thin" # "fat" 更慢但更快;false 最快编译
codegen-units = 1 # 越小优化越好、编译越慢
strip = "symbols" # 去掉符号表,减小体积
panic = "abort"
incremental = false
# 只给测试/基准用更激进的优化
[profile.bench]
opt-level = 3
debug = true
# ── 工作区 ─────────────────────────────────────────────────
[workspace]
members = ["crates/*"]
resolver = "3" # 与 edition 2024 配套的依赖解析器
exclude = ["old-stuff"]
[workspace.package] # 子 crate 用 workspace = true 继承
edition = "2024"
license = "MIT OR Apache-2.0"
[workspace.dependencies] # 统一版本,子 crate 写 { workspace = true }
serde = "1"
tokio = "1"
# ── 其他 ───────────────────────────────────────────────────
[patch.crates-io] # 临时替换某个依赖(调试上游 bug)
my-lib = { path = "../my-lib-fork" }
[replace] # 已废弃,用 [patch] 代替常用的子 crate 继承写法:
toml
# crates/api/Cargo.toml
[package]
name = "api"
version.workspace = true
edition.workspace = true
[dependencies]
serde = { workspace = true, features = ["derive"] }build.rs 最小骨架:
rust
// build.rs:在编译本 crate 之前运行,用于代码生成/探测环境
fn main() {
// 让 cargo 在环境变量变化时重新运行本脚本
println!("cargo::rerun-if-changed=build.rs");
println!("cargo::rerun-if-env-changed=MY_FLAG");
// 向代码注入编译期常量:env!("MY_VALUE")
println!("cargo::rustc-env=MY_VALUE=hello");
}⚠️ 陷阱:
edition与resolver不写时会静默沿用旧默认值, 新项目务必显式声明edition = "2024"。 另外Cargo.lock应提交(二进制项目)—— 它保证构建可复现; 库项目可以提交也可以不提交。
🚀 进阶:
[patch]与[replace]只影响当前工作区的解析结果, 不会影响下游用户。要在发布版里修依赖,得cargo update -p x --precise y或等上游发新版。
C.10 常用 trait 一页纸
| trait | 什么时候用 | 怎么实现 | derive 可用 |
|---|---|---|---|
From<T> | 定义不会失败的类型转换 | 手写 fn from(v: T) -> Self | ❌ |
Into<T> | 作为 From 的反向调用语法 | 不要手写,实现 From 自动获得 | ❌ |
TryFrom<T> | 定义可能失败的转换(缩小数值、解析) | 手写 type Error; fn try_from(..) | ❌ |
TryInto<T> | TryFrom 的反向语法 | 不要手写,实现 TryFrom 自动获得 | ❌ |
FromStr | 让类型支持 "..".parse::<T>() | 手写 type Err; fn from_str(s) -> Result | ❌ |
AsRef<T> | 函数接受「像 T 的引用」的廉价转换(&str/&Path) | 手写 fn as_ref(&self) -> &T | ❌ |
Borrow<T> | 用另一种类型做哈希/比较的键(HashMap<String, _> 用 &str 查) | 手写 fn borrow(&self) -> &T | ❌ |
Deref | 智能指针透明访问目标;不要为继承复用而实现 | 手写 type Target; fn deref(&self) | ❌ |
DerefMut | 同上,可变版本 | 手写 fn deref_mut(&mut self) | ❌ |
Iterator | 定义可惰性遍历的序列 | 手写 type Item; fn next(&mut self) | ❌ |
IntoIterator | 让类型能被 for 循环消费 | type Item; type IntoIter; fn into_iter(self) | ❌ |
Extend<A> | 支持 extend(iter) 与 collect() | 手写 fn extend<I: IntoIterator<Item = A>>(..) | ❌ |
Default | 需要「零配置起始值」 | 手写 fn default() -> Self;或在 #[derive(Default)] 基础上覆盖 | ✅ |
Display | 面向用户的打印({}) | 手写 fn fmt(&self, f: &mut Formatter) | ❌ |
Debug | 面向开发者的打印({:?}、断言、日志) | 通常 #[derive(Debug)] | ✅ |
Clone | 显式深拷贝 .clone() | #[derive(Clone)](字段全 Clone 时) | ✅ |
Copy | 隐式按位复制(要求无 Drop) | #[derive(Copy, Clone)](字段全 Copy) | ✅ |
PartialEq | == / != | #[derive(PartialEq)] 或手写 fn eq | ✅ |
Eq | 声明「相等关系是全等的」(浮点不满足) | #[derive(Eq)],需先有 PartialEq | ✅ |
PartialOrd / Ord | <、>、sort()、BTreeMap 的键 | #[derive(PartialOrd, Ord)](字段顺序决定比较优先级) | ✅ |
Hash | 作为 HashMap / HashSet 的键 | #[derive(Hash)],必须与 Eq 保持一致 | ✅ |
Drop | 释放资源(文件句柄、锁、连接池) | 手写 fn drop(&mut self) | ❌ |
Send | 「可安全地转移到另一个线程」 | 一般不手写;unsafe impl 极少用 | ❌(自动) |
Sync | 「可被多个线程同时引用(&T 是 Send)」 | 一般不手写;用 Mutex 等包装而非 unsafe impl | ❌(自动) |
Error | 作为 Box<dyn Error> / ? 的错误类型 | 手写 Display + Debug + fn source(),或用 thiserror | ❌ |
Fn / FnMut / FnOnce | 接收闭包参数 | 用闭包字面量;手写实现需 nightly | ❌ |
Add / Sub / Mul 等 | 运算符重载 | 手写 type Output; fn add(self, rhs) | ❌ |
rust
use std::collections::HashMap;
use std::fmt;
// Debug + Clone + PartialEq + Eq + Hash + Ord 一次 derive 齐全
// 注意:derive(Ord) 按字段声明顺序比较,想按年龄排序就把 age 放第一位
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
struct User {
age: u32,
name: String,
}
// Display 必须手写
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.name, self.age)
}
}
// From:不失败的转换,顺带白拿 Into(实现了 From 就自动获得 Into)
impl From<(&str, u32)> for User {
fn from((name, age): (&str, u32)) -> Self {
User { age, name: name.to_string() } // 字段顺序与结构体声明一致
}
}
// 用泛型 + AsRef 接受「任何像字符串的东西」,避免强制调用方 clone
impl User {
fn new<S: AsRef<str>>(age: u32, name: S) -> Self {
User { age, name: name.as_ref().to_string() }
}
// AsRef 让函数既能收 &str 也能收 &String
fn rename<S: AsRef<str>>(&mut self, name: S) {
self.name = name.as_ref().to_string();
}
}
fn greet(name: impl AsRef<str>) -> String {
format!("你好,{}", name.as_ref())
}
fn main() {
let mut u = User::new(30, "Alice"); // 借用 &str
let owned = String::from("Bob");
u.rename(&owned); // 也接受 &String
println!("{u}"); // 输出:Bob (30)
// Borrow 的威力:HashMap<String, _> 可以直接用 &str 查,无需构造 String
let mut m: HashMap<String, u32> = HashMap::new();
m.insert(String::from("k"), 1);
println!("{:?}", m.get("k")); // 输出:Some(&1)
println!("{}", greet("世界")); // 输出:你好,世界
println!("{}", greet(owned)); // 输出:你好,Bob
let d = User::default(); // Default 由 derive 提供
println!("{d:?}"); // 输出:User { age: 0, name: "" }
// Ord:可排序
let mut users = vec![User::new(30, "A"), User::new(20, "B")];
users.sort(); // 按 age 升序(字段顺序决定)
println!("{}", users[0].age); // 输出:20
}AsRef vs Borrow vs Deref 快速区分:
| trait | 关键差异 | 典型用途 |
|---|---|---|
AsRef<T> | 只承诺「能给出 &T」,不承诺哈希/相等性与 T 一致 | 函数参数泛化(impl AsRef<Path>) |
Borrow<T> | 额外承诺 Hash/Eq/Ord 与 T 行为一致 | 哈希表用 &str 查 String 键 |
Deref<Target = T> | 提供隐式强制转换(&String → &str) | 智能指针;不要用于模拟继承 |
⚠️ 陷阱:
#[derive(Ord)]严格按字段声明顺序逐字段比较。struct User { name: String, age: u32 }的默认排序是「先按 name」, 想要「先按 age」必须把age写在前面,或手写impl Ord。 同理,derive(Hash)与手写PartialEq不一致会让HashMap行为错乱。
🚀 进阶:
Send/Sync是自动 trait:字段全Send,结构体就自动Send。 用Rc字段会让类型自动失去Send—— 这就是编译器帮你挡住跨线程误用的机制。 需要手写unsafe impl Send时,请先确认真的理解了不变量。
C.11 编译器属性与 lint 速查
| 属性 | 作用 | 何时用 |
|---|---|---|
#[derive(..)] | 自动实现标准 trait | Debug 几乎必加;Clone/PartialEq/Eq/Hash/Default 按需 |
#[inline] | 建议跨 crate 内联 | 短小的热路径访问器;不要滥用(增大体积、拖慢编译) |
#[inline(always)] | 强制内联 | 极少数性能关键的小函数;通常不必要 |
#[cold] | 标记「几乎不会执行」的路径 🚀 | 错误构造、panic 辅助函数,帮助分支预测与布局 |
#[track_caller] 🚀 | 让 panic!/Location::caller() 报告调用方位置 | 自己写 expect 风格的辅助函数时 |
#[allow(lint)] | 抑制某个 lint | 局部、附理由;#[allow(dead_code)] 用于尚未接线的 API |
#[warn(lint)] / #[deny(lint)] | 提升/提升警告级别 | 在模块或 crate 级收紧规则 |
#[expect(lint)](1.81) | 抑制并断言该 lint 确实存在 | 比 allow 更安全:lint 修好后会提示属性过期 |
#[must_use] | 忽略返回值时发出警告 | 返回 Result/Option 或「纯函数」的方法 |
#[non_exhaustive] | 禁止下游穷尽匹配/直接构造 | 公开枚举,预留加变体的余地 |
#[deprecated] | 标记弃用,可带 note/since | 迁移期保留兼容 API |
#[cfg(..)] | 条件编译 | 平台差异、feature 开关、测试专用代码 |
#[cfg_attr(cond, attr)] | 条件地附加属性 | #[cfg_attr(test, derive(Default))] |
#[repr(C)] | 使用 C 的内存布局 | FFI 传结构体给 C;字段顺序与对齐有保证 |
#[repr(transparent)] | 与唯一非零大小字段同布局 | newtype 包装用于 FFI(struct Meters(f64)) |
#[repr(u8)] 等 | 指定枚举判别值的整数类型 | FFI 枚举、紧凑存储 |
#[repr(packed)] | 去掉填充(可能未对齐) | 二进制协议解析;⚠️ 取字段引用会 UB |
#[doc = ".."] / /// | 文档注释 | 公开 API 必须写 |
#[doc(hidden)] | 从文档中隐藏 | 内部实现细节、宏辅助项 |
#[test] / #[bench] | 标记测试/基准 | #[bench] 需 nightly 或 criterion |
#[should_panic] | 断言测试会 panic | 测 panic! 路径;可加 expected = "子串" |
#[ignore] | 默认跳过该测试 | 慢测试、"需要网络" 的测试 |
#[global_allocator] | 指定全局分配器 | 换 jemalloc/mimalloc 提性能 |
#[no_mangle] | 保留符号名供外部链接 | 导出给 C 调用的函数 |
#[no_std](crate 级) | 不链接标准库 | 嵌入式、内核 |
rust
// crate 级 lint 策略:放在 main.rs / lib.rs 的最顶部
#![warn(missing_docs, rust_2018_idioms)]
#![deny(unsafe_op_in_unsafe_fn)]
/// 一个会返回错误的函数:用 #[must_use] 强迫调用方处理返回值
#[must_use = "忽略错误会让失败被静默吞掉"]
fn checked_div(a: i32, b: i32) -> Option<i32> {
if b == 0 { None } else { Some(a / b) }
}
/// 自定义断言辅助函数:用 #[track_caller] 让 panic 指向调用方那一行
#[track_caller]
fn expect_positive(n: i32) -> i32 {
if n > 0 {
n
} else {
// Location::caller() 报告的是「调用 expect_positive 的地方」
panic!("期望正数,实际得到 {n}");
}
}
// 公开枚举加 #[non_exhaustive]:下游 match 必须带 _ 分支
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
Active,
Inactive,
}
/// 用 #[repr(transparent)] 做 FFI 安全的 newtype
#[repr(transparent)]
pub struct Handle(pub u64);
/// 平台差异:只在 Windows 上编译
#[cfg(windows)]
fn platform_name() -> &'static str {
"windows"
}
#[cfg(not(windows))]
fn platform_name() -> &'static str {
"unix-like"
}
/// 旧接口:保留但提示迁移
#[deprecated(since = "0.2.0", note = "请改用 checked_div")]
fn old_div(a: i32, b: i32) -> i32 {
a / b
}
/// 冷路径示例:错误构造几乎不执行,标记后利于指令布局
#[cold]
fn build_error(msg: &str) -> String {
format!("error: {msg}")
}
fn main() {
// #[must_use]:取消注释这行会得到 unused_must_use 警告
// checked_div(1, 0);
println!("{:?}", checked_div(6, 3)); // 输出:Some(2)
println!("{:?}", Status::Active); // 输出:Active
println!("{}", platform_name()); // 输出:windows(Windows 上实测)
println!("{}", Handle(7).0); // 输出:7
println!("{}", build_error("boom")); // 输出:error: boom
#[allow(deprecated)] // 局部抑制,附理由:演示迁移
{
println!("{}", old_div(6, 3)); // 输出:2
}
let _ = expect_positive(1);
println!("{:?}", checked_div(1, 0)); // 输出:None
}常用 lint 速查:
| lint | 默认级别 | 含义与处置 |
|---|---|---|
unused_variables | warn | 用 _x 或删掉 |
unused_imports | warn | cargo fix 可自动清 |
dead_code | warn | 未使用的项;接口预留可 #[allow(dead_code)] |
unused_must_use | warn | 忽略了 #[must_use] 的返回值 |
unreachable_patterns | warn | match 里有永远匹配不到的分支 |
unused_mut | warn | mut 是多余的,删掉(顺便收紧了约束) |
clippy::needless_range_loop | warn(clippy) | 用迭代器取代 for i in 0..v.len() |
clippy::redundant_clone | warn(clippy) | 多余的 clone,常有零拷贝替代 |
clippy::large_enum_variant | warn(clippy) | 变体大小悬殊,考虑 Box 大变体 |
clippy::too_many_arguments | warn(clippy) | 参数超过 7 个,考虑引入配置结构体 |
unsafe_op_in_unsafe_fn | allow(2024 起 warn) | unsafe fn 内的操作也要显式 unsafe {} |
toml
# Cargo.toml:把 lint 配置固化到工程里(需要 Cargo 1.74+)
[lints.rust]
unsafe_op_in_unsafe_fn = "deny"
missing_docs = "warn"
[lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
module_name_repetitions = "allow" # 按需放宽🧠 原理:
#[inline]只是建议,LLVM 有最终决定权; 泛型与#[inline]函数默认对下游 crate 可见(需要单态化), 而普通函数默认不跨 crate 内联 —— 这才是「为什么需要手动#[inline]」的真正原因。
⚠️ 陷阱:
#[repr(packed)]会产生未对齐字段, 对它取引用(&s.field)是未定义行为(UB)。 必须用std::ptr::addr_of!或先整体read_unaligned复制出来。
延伸阅读
- The Rust Programming Language · 官方书(英文,官方)—— 查语法与概念的标准答案。
- Rust 标准库 API 文档(英文,官方)—— 本附录所有方法签名的权威来源。
- Rust by Example(英文,官方)—— 想看「可运行的最小例子」时。
- The Cargo Book(英文,官方)——
Cargo.toml字段与子命令的完整清单。 - Clippy lint 列表(英文,官方)—— 每条 lint 的说明与反例。
- Rust 程序设计语言 中文版(中文,社区译版)—— 英文吃力时的对照读物。
- Rust 语言圣经(中文,社区)—— 中文速查与「坑」的补充。
- 附录 D · 学习资源与文档索引 —— 本附录所用的全部外部资源入口。
⚠️ 提醒:本附录核对时间为 2026 年(基准
rustc 1.98.1)。 语言与标准库仍在演进,遇到签名不符请以官方doc.rust-lang.org上的当前文档为准; 示例中的第三方 crate 版本也请以cargo add拉到的实际版本为准。