Skip to content

附录 C · 语法与标准库速查卡

本附录是边写代码边查的高密度速查卡:全部以表格与极短代码片段组织,不展开讲解。 需要原理与推导请回到对应正文章节;术语译法统一遵循 附录 A · 术语中英对照

基准环境:rustc 1.98.1 / cargo 1.98.1Rust 2024 edition,Windows 11 + PowerShell。 片段中标注的最小稳定版本(如 1.75)表示该写法在该版本起可用。

怎么用这张卡

你在做什么直接跳
忘了语法的「骨架」长什么样C.1 语法骨架
println! 里那串花括号不会写C.2 格式化输出
不确定该用哪个整数类型 / 怎么转C.3 类型
想找某个集合的方法名与复杂度C.4 集合操作
数据要「一条链」处理完C.5 迭代器
少写几层 matchC.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.rs2024 edition 下二选一,不再需要 mod.rs
目录模块mod m; + src/m/mod.rssrc/m.rs有子模块时推荐 src/m.rs + src/m/sub.rs
绝对路径crate::a::b / ::std::mem::swapcrate:: 指当前 crate 根;::std 指外部 crate
相对路径self::a::b / super::c / super::super::dself 是本模块,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 letif let Some(v) = o { ... } else { ... }单分支匹配的语法糖
while letwhile 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 Traitdyn 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 }
泛型结构体的 implimpl<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 }结构体借用外部数据时必须声明
结构体 implimpl<'a> S<'a> { ... }impl 也要重复声明
返回引用fn f<'a>(x: &'a str) -> &'a str输出生命周期必须是某个输入的子集
静态&'static str整个程序存活,字面量默认如此
省略(elision)fn f(x: &str) -> &str单输入引用 ⇒ 输出自动同寿命
省略(&selffn 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 }层层嵌套
guardmatch n { x if x % 2 == 0 => "even", _ => "odd" }if 附加条件,可引用绑定的名字
忽略剩余match p { Point { x, .. } => x }.. 忽略其余字段
引用匹配match &opt { Some(s) => s.len(), None => 0 }注意匹配的是 &Option<T>
默认 ergonomicsmatch 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 约束能做什么捕获方式
FnFn(Args) -> Out可被多次调用,且不改变捕获状态只读捕获(&T
FnMutFnMut(Args) -> Out可被多次调用,可修改捕获状态可变捕获(&mut T
FnOnceFnOnce(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;fmut
移动捕获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) -> TokenStreamm!(..)自由语法
toml
# 过程宏 crate 的 Cargo.toml 必须声明
[lib]
proc-macro = true
rust
// 声明宏:实现一个简易的 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}右对齐,总宽 8format!("{:>8}", "hi")␣␣␣␣␣␣hi
{:<8}左对齐,总宽 8format!("{:<8}", "hi")hi␣␣␣␣␣␣
{:^8}居中,总宽 8format!("{:^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!返回 StringString
write!写入 impl Write/impl fmt::Writeio::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 标量类型位宽与范围

类型位宽范围 / 取值默认/常用场景
i88−128 ~ 127紧凑数组、字节级计算
i1616−32 768 ~ 32 767音频采样
i3232−2 147 483 648 ~ 2 147 483 647整数字面量默认类型
i6464−9.22×10¹⁸ ~ 9.22×10¹⁸一般计算、时间戳
i128128±1.70×10³⁸大整数、UUID 运算
isize指针宽度(64 位平台为 64)i64(64 位平台)索引差值、len 相关运算
u880 ~ 255字节(Vec<u8>&[u8]
u16160 ~ 65 535端口号、UTF-16 码元
u32320 ~ 4 294 967 295计数、位掩码
u64640 ~ 1.84×10¹⁹哈希值、纳秒时间戳
u1281280 ~ 3.40×10³⁸大整数、IPv6 数值化
usize指针宽度(64 位平台为 64)0 ~ 1.84×10¹⁹(64 位平台)索引与长度必须用它
f3232±1.18×10⁻³⁸ ~ ±3.40×10³⁸,7 位有效数字GPU、内存敏感场景
f6464±2.23×10⁻³⁰⁸ ~ ±1.80×10³⁰⁸,15 位有效数字浮点字面量默认类型
char32一个 Unicode 标量值(U+0000 ~ U+10FFFF,不含代理对)字符,4 字节
bool8true / false布尔
()0唯一值 ()「无返回值」的返回值
!无值(never type)panic!loopbreak 的类型

整数字面量的便捷写法

写法含义等价
1_000_000下划线分隔1000000
0xFF十六进制255
0o77八进制63
0b1010二进制10
b'A'字节字面量65u8
1u8 / 1_i64后缀标注类型显式类型
1.0f32浮点后缀1.0f32

⚠️ 陷阱:索引必须usizev[0i32] 会报 E0277,需要 v[0usize] 或直接写 v[0](字面量可推断)。 反过来,把 usizeu32 用(如 fits u32 的 FFI 字段)必须显式 as 转换。

🧠 原理i32/f64 之所以是默认推断类型,是因为它们在 64 位平台上性能与体积平衡最好。 但溢出行为不同i32 等整数运算在 debug 构建下溢出会 panic, 在 release 构建下静默回绕(wrapping)。想固定语义请用 wrapping_add / checked_add / saturating_add / overflowing_add

C.3.2 类型转换矩阵

手段语法适用场景失败行为会丢数据吗
asx as u8数值间截断/位重解释、指针转整数不失败,静默截断可能(高位丢弃)
Fromu32::from(x) / T::from(x)不会失败的放大转换(u8 → u32编译期就不可能失败不会
Intolet y: u32 = x.into();From 的反向写法,更易读同上不会
TryFromu8::try_from(x)?可能失败的缩小转换(u32 → u8返回 Result不会(失败即 Err)
TryIntolet y: u8 = x.try_into()?;TryFrom 的反向写法返回 Result不会
parse"42".parse::<i32>()?字符串 → 数值/其他类型返回 Result不适用
to_string / format!x.to_string()任意 DisplayString不失败不适用
as_bytes / from_utf8s.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 / TryFrom
rust
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 互转全表

方向写法是否分配备注
&strStringString::from(s)✅ 分配最明确的写法
&strStrings.to_string()来自 ToStringDisplay 的赠品)
&strStrings.to_owned()语义上是「借用的自有版本」,Clone 风格
&strStrings.into()目标类型可由上下文推断时最简洁
String&str&s❌ 零拷贝最常用,靠 Deref 自动转换
String&strs.as_str()显式版,用在泛型/推断歧义处
String&str&s[..]切片语法,可同时取子串
String&[u8]s.as_bytes()直接看底层字节
StringVec<u8>s.into_bytes()❌ 转移消费 String,零拷贝拿到缓冲区
&[u8]&strstd::str::from_utf8(b)?校验 UTF-8,返回 Result
&[u8]&strunsafe { std::str::from_utf8_unchecked(b) }跳过校验,需自行保证
Vec<u8>StringString::from_utf8(v)?❌ 转移校验 UTF-8
Vec<u8>StringString::from_utf8_lossy(&v)可能 ✅非法字节替换为 U+FFFD,返回 Cow<str>
&[u8]StringString::from_utf8_lossy(b).into_owned()一定拿到自有 String
charStringc.to_string()
Stringchars.chars().next()String 不能直接转 char
String + &strs + "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 的 StringStringBuilder 是两种类型,Python 的 str 不可变、 拼接必须靠 join。Rust 的 String可变且拥有所有权的,&str 是借用的视图 —— 这正对应 C++ 的 std::stringstd::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] = xO(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 / vO(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_cmppartial_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 &mapO(n)顺序不保证
键集合keys()O(n)
值集合values() / values_mut()O(n)
条目 APIentry(k).or_insert(v)平均 O(1)一次哈希搞定「查或插」
条目 APIentry(k).or_insert_with(f)平均 O(1)惰性构造默认值
条目 APIentry(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 &setO(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 / retainO(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 / removeO(log n)HashMap 同名同签名
条目 APIentry(k).or_insert(v)O(log n)用法与 HashMap 一致
遍历for (k, v) in &mapO(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哈希表结构上做不到
键是浮点或无 OrdHashMap只需 Hash + Eq
输出必须可复现(测试、快照)BTreeMap顺序确定
抗 HashDoS 最稳BTreeMap最坏也是 O(log n)

C.4.6 String / &str 常用方法

操作方法复杂度备注
新建空串String::new()O(1)
预留容量String::with_capacity(n)O(1)拼接前必做
追加 &strpush_str(s)摊还 O(k)原地
追加 charpush(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 上叠一层包装;必须由消费器驱动。

适配器签名/用途备注
mapmap(f)Iterator<Item = U>一对一变换,最常用
filterfilter(pred)保留 pred(&item) == true 的项
filter_mapfilter_map(f: FnMut(T) -> Option<U>)一步完成「映射 + 丢弃 None
flat_mapflat_map(f: FnMut(T) -> IntoIterator)映射后展平一层
flattenflatten()展平嵌套的 Iterator<Item: IntoIterator>
enumerateenumerate()(usize, T)带下标
zipzip(other)(A, B)以短的一方为准截断
chainchain(other)顺序拼接两个迭代器
revrev()反向,要求 DoubleEndedIterator
taketake(n)只取前 n 个
take_whiletake_while(pred)直到 pred 为 false(含首个 false 之后全丢)
skipskip(n)跳过前 n 个
skip_whileskip_while(pred)跳过开头满足条件的项
step_bystep_by(n)每 n 个取一个(n 不能为 0)
peekablepeekable()Peekable.peek() 看下一个而不消费
inspectinspect(f)借用每项做副作用(日志),不改变数据
scanscan(init, f)带状态的 mapf 返回 Option 可提前终止
cyclecycle()无限重复(需配合 take
cloned / copiedcloned() / copied()&TTcopied 要求 T: Copy
by_refit.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,立即执行并产出结果)

消费器签名/用途复杂度
collectcollect::<Vec<_>>() / ::<HashMap<_,_>>()O(n)
collect(Result)collect::<Result<Vec<_>, _>>()O(n),遇 Err 短路
foldfold(init, f)O(n),万能归约
reducereduce(f)O(n),无初值版本,返回 Option
try_foldtry_fold(init, f) -> ResultO(n),可提前短路
sum / productsum::<i32>()O(n)
countcount()O(n)ExactSizeIteratorO(1)
for_eachfor_each(f)O(n),代替 for 循环用在链尾
any / allany(pred) / all(pred)短路
find / find_mapfind(pred)Option<T>短路
positionposition(pred)Option<usize>短路
max / minmax() / min()Option<T>O(n),需 Ord
max_by_key / min_by_keymax_by_key(f)O(n)
max_by / min_bymax_by(cmp)O(n),自定义比较
lastlast()Option<T>O(n)
nthnth(2)Option<T>消费掉前面所有项
take(n).collect()取前 n 个O(n)
partitionpartition::<Vec<_>, _>(pred)(Vec, Vec)O(n)
unzipunzip::<_, _, Vec<_>, Vec<_>>()O(n)Vec<(A,B)>(Vec<A>, Vec<B>)
zip + forfor (a, b) in x.iter().zip(y)O(n)
all/any 混用注意any 空集返回 falseall 空集返回 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 会因「不能把 &i32i32」而报错, 需要解引用 *x 或加 .copied()

🧠 原理:迭代器链之所以「零开销」,是因为编译器会把每一层 map/filter 内联成单个循环,与手写 for 生成的机器码基本等价(〈工程化与常用 crate〉一章会实测)。 唯一的例外是 collect() 分配,以及 dyn Iterator 装箱。


C.6 Option / Result 组合子速查

C.6.1 Option<T> 组合子

组合子签名/用途备注
is_some / is_nonebool只判断,不取出
mapmap(f: FnOnce(T) -> U)Option<U>变换「有值」的情况
map_ormap_or(default, f)有值则 f,否则返回 default
map_or_elsemap_or_else(default_fn, f)default 也惰性计算
and_thenand_then(f: FnOnce(T) -> Option<U>)链式组合,避免 Some(Some(x)) 嵌套
andand(optb)有值则返回 optb,否则 None
oror(optb)无值则返回 optb
or_elseor_else(f: FnOnce() -> Option<T>)惰性备选值
filterfilter(pred)值为 None 或条件不满足则丢弃
unwrap取出或 panic仅用于「不可能失败」处,且附 expect 理由
expect同上,panic 时带自定义信息优于 unwrap
unwrap_or取出或返回默认值饥渴求值
unwrap_or_default取出或 T::default()T: Default
unwrap_or_else惰性生成默认值默认值昂贵时用
ok_orok_or(err)Result<T, E>饥渴求值
ok_or_elseok_or_else(f)Result<T, E>惰性构造错误,推荐
transposeOption<Result<T, E>>Result<Option<T>, E>两种「可能失败的可选值」互转
zipzip(other)Option<(A, B)>两者都有才有值
taketake() → 取出并留下 None&mut Option<T>
replacereplace(v) → 旧值&mut Option<T>
get_or_insert为空则先插入&mut Option<T>
iter / into_iterOption 当 0/1 元素迭代器便于接 flatten
as_ref / as_mutOption<&T> / Option<&mut T>不消费原 Option
as_derefOption<&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_errbool判断
mapmap(f)Result<U, E>只变换 Ok
map_errmap_err(f)Result<T, F>只变换错误,错误类型转换主力
map_or / map_or_else带默认值
and_thenand_then(f: FnOnce(T) -> Result<U, E>)链式组合,E 必须同为一种
or_elseor_else(f: FnOnce(E) -> Result<T, F>)错误恢复/降级
unwrap / expect取出或 panic原型阶段可用,库代码避免
unwrap_or / unwrap_or_default / unwrap_or_else兜底值
okResult<T, E>Option<T>丢弃错误
errOption<E>丢弃成功值
transposeOption<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_mutResult<&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_elseOption 升格为 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 样板太多,惯例是 库代码用 thiserrorcargo add thiserror)定义错误枚举, 应用代码用 anyhowcargo 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_mutRc::ptr_eq单线程共享只读所有权(图、树共享节点)
Arc<T>Arc::new(v)Arc::clone(&a)Arc::strong_countArc::ptr_eq多线程共享只读所有权;Send + Sync
RefCell<T>RefCell::new(v)borrow()borrow_mut()try_borrow()replaceinto_inner单线程内部可变性,借用规则挪到运行期检查
Cell<T>Cell::new(v)get()T: Copy)、setreplacetake单线程、T: Copy 或被 move 出的内部可变性
Mutex<T>Mutex::new(v)lock()LockResult<MutexGuard>try_lockinto_inner多线程互斥访问可变数据
RwLock<T>RwLock::new(v)read()write()try_readtry_write读多写少;读并行
AtomicUsizeAtomicUsize::new(0)loadstorefetch_addcompare_exchangeswap无锁计数/标志;需指定 Ordering
OnceLock<T>(1.70)OnceLock::new()getsetget_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); 读多写少用 RwLockMutex 的保护范围应尽量小 —— 别在持锁时做 IO。

C.7.3 thread::spawnthread::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::spawnthread::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 原子类型

类型常用方法备注
AtomicBoolload / store / swap / compare_exchange标志位
AtomicUsize / AtomicIsizefetch_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 常用函数

操作写法备注
读整个文件为 Stringstd::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")?覆盖写,自动创建/截断
追加写见下方 OpenOptionsfs::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 / symlinkWindows 上 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)
&strp.to_str()Option<&str>非 UTF-8 时是 None
损失式转换p.to_string_lossy()Cow<str>非法字节替换为 U+FFFD
遍历组件p.components()平台无关地逐段处理
由字符串解析PathBuf::from(s) / p.push("x")push 就地修改
转为 Stringp.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()不等待,返回 Childchild.wait() / kill()
ExitStatus::success() / .code()退出状态;被信号杀死时 code()None

⚠️ 陷阱Command::new 不经过 shell,所以 Command::new("echo hi") 会失败 (没这个可执行文件),管道、通配符、&& 也都不生效。 需要 shell 语义时显式调用 cmd /C(Windows)或 sh -c(Unix), 并且绝不要把用户输入直接拼进 shell 命令字符串 —— 那是命令注入。

🚀 进阶:想要更友好的进程管理可用 ductxshell; 想要类型化的 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_varunsafe 函数: 在有多线程时修改变量会与 getenv 竞态,能用配置结构体就别用环境变量。 真实 CLI 建议用 clapcargo 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 clippylint 检查--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 rustcrustc 传额外参数-- -Zunpretty=expanded(nightly)
cargo login / cargo logout管理 crates.io tokentoken 存在 ~/.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-nextestcargo install cargo-nextest更快的测试运行器,cargo nextest run
cargo-expandcargo install cargo-expand展开宏,cargo expand
cargo-llvm-covcargo install cargo-llvm-cov覆盖率,cargo llvm-cov --html
cargo-flamegraphcargo install cargo-flamegraph生成火焰图,cargo flamegraph
cargo-auditcargo install cargo-audit检查依赖的已知漏洞,cargo audit
cargo-denycargo install cargo-deny依赖许可证/来源/重复版本策略
cargo-watch / baconcargo install bacon文件变化自动重跑检查
cargo-mirirustup 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");
}

⚠️ 陷阱editionresolver 不写时会静默沿用旧默认值, 新项目务必显式声明 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「可被多个线程同时引用&TSend)」一般不手写;用 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/OrdT 行为一致哈希表用 &strString
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(..)]自动实现标准 traitDebug 几乎必加;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]断言测试会 panicpanic! 路径;可加 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_variableswarn_x 或删掉
unused_importswarncargo fix 可自动清
dead_codewarn未使用的项;接口预留可 #[allow(dead_code)]
unused_must_usewarn忽略了 #[must_use] 的返回值
unreachable_patternswarnmatch 里有永远匹配不到的分支
unused_mutwarnmut 是多余的,删掉(顺便收紧了约束)
clippy::needless_range_loopwarn(clippy)用迭代器取代 for i in 0..v.len()
clippy::redundant_clonewarn(clippy)多余的 clone,常有零拷贝替代
clippy::large_enum_variantwarn(clippy)变体大小悬殊,考虑 Box 大变体
clippy::too_many_argumentswarn(clippy)参数超过 7 个,考虑引入配置结构体
unsafe_op_in_unsafe_fnallow(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 复制出来。


延伸阅读

⚠️ 提醒:本附录核对时间为 2026 年(基准 rustc 1.98.1)。 语言与标准库仍在演进,遇到签名不符请以官方 doc.rust-lang.org 上的当前文档为准; 示例中的第三方 crate 版本也请以 cargo add 拉到的实际版本为准。

内容以 rustc 1.98.1 · Rust 2024 edition 为基准