跳到主要内容

Rust 速查表,100+ 段地道 Rust 代码,覆盖所有权、生命周期、trait、async 与常见坑

Rust 速查表,100+ 段地道 Rust 代码,所有权、借用、生命周期、trait、async、错误处理。

  • 本地处理
  • 分类 开发运维
  • 适合 格式化、校验、压缩或检查和代码相关的文本。
分类:
165
基础 (15)

main 与 println!

fn main() {
    println!("hello, world");
    let name = "Rust";
    println!("hi, {name}");                 // 1.58+ 直接捕获作用域变量
    println!("{} + {} = {}", 1, 2, 1 + 2);  // 位置参数
}

Rust 二进制从 fn main 开始执行。println! 是宏(注意感叹号)不是函数;1.58 起可以直接用 {name} 把作用域里的变量插值进去。

let、mut 与变量遮蔽

let x = 5;                  // 不可变
let mut y = 5;              // 可变
y += 1;

// shadowing:用 let 重新绑定,类型可变
let s = "42";
let s: i32 = s.parse().unwrap();  // s 现在是 i32

绑定默认不可变,要改加 mut。shadowing 是用 let 重新建一个绑定,类型可以变,跟赋值不是一回事。

整数 / 浮点 / 布尔 / 字符类型

let a: i32 = -1;            // i8 i16 i32 i64 i128 isize
let b: u64 = 1;             // u8 u16 u32 u64 u128 usize
let c: f64 = 3.14;          // 默认浮点 f64
let d: bool = true;
let e: char = '中';         // 4 字节 Unicode 标量值

let big = 1_000_000_u32;    // 数字字面量可加下划线、加类型后缀

Rust 整型显式标符号和位宽。char 是 4 字节 Unicode 标量值,不是一个字节。字面量里下划线分组、加后缀固定类型,能让代码更好读。

元组、数组、切片

let t: (i32, &str, f64) = (1, "two", 3.0);
let (a, _, c) = t;          // 解构
println!("{} {}", a, c);

let arr: [i32; 4] = [10, 20, 30, 40];   // 长度是类型一部分
let s: &[i32] = &arr[1..3];             // 切片:胖指针 (ptr, len)

元组定长可异构;数组定长同类型(长度是类型的一部分);切片 &[T] 是胖指针(数据指针 + 长度),指向数组或 Vec 的一段。

String 与 &str 的区别

let lit: &str = "hello";            // 字面量,'static
let owned: String = String::from(lit);
let owned2 = lit.to_string();
let borrowed: &str = &owned;        // String 可以解引用成 &str

fn takes(s: &str) { println!("{s}") }  // 参数一律收 &str
takes(&owned);
takes(lit);

String 是堆上可增长的 UTF-8 拥有式缓冲。&str 是借用的 UTF-8 字节视图(切片)。函数参数一般写 &str,这样两种都能传。

if 作为表达式

let n = 7;
let parity = if n % 2 == 0 { "even" } else { "odd" };
println!("{parity}");

// 分支类型必须一致;少写 else 编译器会按 unit 类型给你 ()
let _u = if n > 0 { /* () */ };

if 是表达式,会返回值。所有分支类型必须一致。漏掉 else 时返回值是 unit 类型 ()。

loop / while / for 区间循环

let mut i = 0;
let answer = loop {
    i += 1;
    if i == 10 { break i * i }      // loop 也可返回值
};

while i < 20 { i += 1 }

for n in 0..5 { print!("{n} ") }    // 0..5 不含 5
for n in 0..=5 { print!("{n} ") }   // 0..=5 含 5
for c in "abc".chars() { print!("{c}") }

loop 是死循环,break 可以带返回值。for 遍历任何实现 IntoIterator 的东西,区间 0..n 和 0..=n、以及 .chars()、.iter()、.iter_mut() 等等。

fn 函数定义

fn add(a: i32, b: i32) -> i32 {
    a + b                       // 最后一行无分号 = 返回值
}

fn divmod(a: i64, b: i64) -> (i64, i64) {
    (a / b, a % b)              // 返回元组 = 多返回值
}

// early return 用 return; 最后一行别加分号否则变成 ()
fn abs(x: i32) -> i32 {
    if x < 0 { return -x; }
    x
}

函数参数和返回类型都要写。最后一行表达式(不带分号)就是返回值;提前返回用 return。多返回值就用元组。

match 穷尽模式匹配

let n = 3;
let label = match n {
    0 => "zero",
    1 | 2 => "one or two",
    3..=9 => "single digit",
    _ => "big",                 // _ 兜底,否则编译期报 non-exhaustive
};
println!("{label}");

// 解构 Option
let opt: Option<i32> = Some(7);
match opt {
    Some(x) if x > 0 => println!("pos {x}"),  // match guard
    Some(_) | None => println!("other"),
}

match 必须穷尽,漏分支编译期就报错。支持字面量、区间 1..=9、或模式 1 | 2、守卫 if 条件,以及解构 enum / struct / tuple。

const 与 static 的区别

const MAX_RETRY: u32 = 5;           // 编译期内联,无固定地址
const PI: f64 = 3.141_592_653_589_793;

static APP_NAME: &str = "toolora";  // 程序运行期间有固定内存地址
static mut COUNTER: u32 = 0;        // 可变 static 必须 unsafe 访问

const 在编译期被内联到使用处(没有固定地址)。static 在程序运行期间有唯一内存地址。static mut 访问必须 unsafe,一般用 OnceLock / Mutex 替代。

if let 与 let else

let opt: Option<i32> = Some(7);

// if let:只关心一个分支
if let Some(x) = opt {
    println!("got {x}");
}

// let else:解构失败就走发散分支(return / break / panic)
fn parse(s: &str) -> i32 {
    let Ok(n) = s.parse::<i32>() else {
        return -1;
    };
    n
}
println!("{}", parse("42"));

if let 就是只关心一个分支的 match。let else(1.65 稳定)匹配成功就绑定,失败就走发散块(return / break / panic),写提前返回不用层层嵌套。

带标签循环与 break 返回值

let found = 'outer: loop {
    for i in 0..10 {
        for j in 0..10 {
            if i * j == 12 {
                break 'outer (i, j);   // 跳出外层并带返回值
            }
        }
    }
};
println!("{found:?}");                  // (2, 6)

循环标签 'name 让 break / continue 指向外层循环。break 还能从循环表达式里带出一个值。两者结合,一条语句就能带着结果跳出指定层。

as 类型转换

let x = 3.9_f64;
let y = x as i32;               // 截断小数 → 3,不四舍五入
let b = 300_i32 as u8;          // 溢出回绕 → 44 (300 % 256)
let c = 'A' as u32;             // char → 码点 65
let back = 97_u8 as char;       // u8 → char 'a'
println!("{y} {b} {c} {back}");

as 做廉价的原始数值转换:浮点转整型向零截断,缩窄会回绕(绝不 panic),char 转整型得到码点。可能失败的转换优先用 TryFrom / try_into。

块作为表达式

let area = {
    let w = 3;
    let h = 4;
    w * h                       // 最后一行无分号 = 块的值
};
println!("{area}");             // 12

// 用块把临时变量限制在小作用域里
let cleaned = {
    let raw = "  hi  ";
    raw.trim().to_uppercase()
};
println!("{cleaned}");

{ ... } 块是个表达式,求值结果是块里最后一个表达式(不带分号)。用它把临时变量限制在小作用域,不写辅助函数也能内联算出一个值。

while let 循环

let mut stack = vec![1, 2, 3];

// pop 返回 Option,Some 就继续循环,None 自动停
while let Some(top) = stack.pop() {
    println!("{top}");          // 3, 2, 1
}

while let 在模式持续匹配时一直循环,最适合用 .pop()(返回 Option)排空栈 / 队列,或消费迭代器直到产出 None。

所有权 (12)

默认按 move 转移所有权

let s1 = String::from("hi");
let s2 = s1;                // s1 的所有权 move 给 s2
// println!("{s1}");        // ❌ borrow of moved value

let n1 = 5;
let n2 = n1;                // i32 实现了 Copy,按位拷贝
println!("{n1} {n2}");      // ✅ 都还能用

对非 Copy 类型(如 String)来说,赋值 / 传参就是 move 所有权,源绑定不能再用。Copy 类型(整型、bool、f64、&T 等)按位拷贝,两边都能继续用。

共享借用 &T

fn len(s: &String) -> usize { s.len() }

let s = String::from("hello");
let n = len(&s);            // 借用,不转移所有权
println!("{s} 长度 {n}");   // ✅ s 还在

// 同时存在多个 &T 是允许的
let r1 = &s;
let r2 = &s;
println!("{r1} {r2}");

&T 是共享(只读)借用。可以同时存在多个。它不转移所有权,调用结束原绑定还能继续用。

独占借用 &mut T

fn push_world(s: &mut String) {
    s.push_str(", world");
}

let mut s = String::from("hello");
push_world(&mut s);
println!("{s}");

// 同一作用域同一时间只能存在 ONE &mut T
let r1 = &mut s;
// let r2 = &mut s;          // ❌ second mutable borrow
r1.push('!');

&mut T 是独占(可写)借用。同一时刻只能有一个,并且不能跟任何 & 借用同时存在。这条规则在编译期就消灭数据竞争。

Copy 与 Clone 的区别

#[derive(Copy, Clone, Debug)]
struct Point { x: i32, y: i32 }     // 小、栈上 → Copy 合理

#[derive(Clone, Debug)]
struct Profile { name: String }     // 堆数据 → 只能 Clone

let p = Point { x: 1, y: 2 };
let q = p;                          // 隐式 Copy
println!("{p:?} {q:?}");

let a = Profile { name: "li".into() };
let b = a.clone();                  // 必须显式 .clone()
println!("{} {}", a.name, b.name);

Copy 隐式按位拷贝,只能对所有字段也都是 Copy 的类型实现。Clone 显式调 .clone(),可以做任意工作(比如分配内存)。堆上的 String / Vec 这种是 Clone 但不是 Copy。

借用检查器的两条铁律

// 两条铁律:
// 1) 同一时间,要么 N 个 &T,要么 1 个 &mut T,不能混
// 2) 任何借用的生命周期不得超过被借数据本身

let mut v = vec![1, 2, 3];
let r = &v[0];              // 不可变借用 v
// v.push(4);               // ❌ push 需要 &mut self
println!("{r}");            // r 用完,借用结束
v.push(4);                  // ✅ 现在可以
println!("{v:?}");

两条铁律:(1) 同一时间要么 N 个 &T 要么 1 个 &mut T,绝不混;(2) 任何引用都不能比它指向的数据活得更久。NLL(非词法生命周期)让借用在最后一次使用后立即结束,多数"跟借用检查器打架"只要按自然流程重排就过了。

用引用避免不必要 clone

// ❌ 不必要的 clone
fn greet_bad(name: String) { println!("hi, {name}") }
let s = String::from("li");
greet_bad(s.clone());       // 多拷一份堆内存

// ✅ 借用
fn greet(name: &str) { println!("hi, {name}") }
greet(&s);                  // 零成本
greet("literal");           // 字面量也直接传

函数只是读的话,签名写 &str / &[T] / &T,不要 String / Vec / T。这样既省掉 .clone() 的堆分配,又能同时接受拥有式和借用式的调用方。

Drop trait,RAII 资源回收

struct Guard(&'static str);

impl Drop for Guard {
    fn drop(&mut self) {
        println!("dropping {}", self.0);
    }
}

fn main() {
    let _a = Guard("outer");
    {
        let _b = Guard("inner");
    }                       // 这里打印 dropping inner
    // 函数结束打印 dropping outer
}

变量离开作用域时 Rust 自动调用 Drop::drop。文件关闭、锁释放、内存回收都靠这条,没有 GC。同一作用域里 drop 顺序是 LIFO。

std::mem::take 与 replace

use std::mem;

struct Buf { data: Vec<u8> }

impl Buf {
    // 拿走 self.data,把它替换成默认值(空 Vec)
    fn drain(&mut self) -> Vec<u8> {
        mem::take(&mut self.data)
    }
}

let mut b = Buf { data: vec![1, 2, 3] };
let out = b.drain();
assert!(b.data.is_empty());
println!("{out:?}");

mem::take 从 &mut 引用里取出一个值,留下 Default::default()。mem::replace 让你换入任意值。两个都允许从借用的结构体里 move 出值,又不违反借用规则。

从结构体里部分 move

struct Pair { a: String, b: String }
let p = Pair { a: "x".into(), b: "y".into() };

let a = p.a;                    // move 出 p.a
// println!("{}", p.a);         // ❌ p.a 已被 move
println!("{}", p.b);            // ✅ p.b 还能用
// println!("{p:?}");           // ❌ p 整体已部分 move

可以单独把结构体里的非 Copy 字段 move 出来;之后那个字段处于未初始化状态,但其它字段照常能用。不过整个结构体作为一个整体就不能再被 move 或整体借用了。

&mut 的再借用

fn bump(n: &mut i32) { *n += 1 }

let mut x = 0;
let r = &mut x;
bump(r);            // 这里是再借用 &mut *r,不是 move 走 r
bump(r);            // ✅ r 还能继续用
println!("{x}");    // 2

把 &mut T 传给收 &mut T 的函数时发生隐式再借用(&mut *r),不是 move,所以原来的 &mut 绑定之后还能用。这就是为什么能连着调两次 bump(r)。

Vec into_iter 消费所有权

let names = vec![String::from("a"), String::from("b")];

// into_iter() 产出 String(拥有),把元素 move 出来
let upper: Vec<String> = names.into_iter()
    .map(|s| s.to_uppercase())
    .collect();
// println!("{names:?}");       // ❌ names 已被消费
println!("{upper:?}");

对拥有式 Vec 调 .into_iter() 产出拥有式 T 并消费整个 Vec,让你不用克隆就能把每个元素 move 出来。转换集合且不再需要原集合时用它。

Default 与 ..Default::default()

#[derive(Debug, Default)]
struct Opts {
    verbose: bool,
    retries: u32,
    name: String,
}

let o = Opts { retries: 3, ..Default::default() };
println!("{o:?}");              // verbose:false retries:3 name:""

derive Default 让 Opts::default() 给出零值 / 空值。配合结构体更新语法 ..Default::default(),只设你关心的字段,是配置结构体很干净的轻量 builder 写法。

生命周期 (9)

生命周期标注 'a 基础

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

let s1 = String::from("longer string");
let s2 = "short";
let r = longest(&s1, s2);
println!("{r}");

生命周期标注描述引用之间的关系,不改变任何东西的实际存活时间。这里的 'a 表示:返回的引用在两个入参都还活着的时间段内有效。

生命周期省略规则

// 编译器按 3 条规则自动补全,这两段等价:
fn first(s: &str) -> &str { &s[..1] }
fn first2<'a>(s: &'a str) -> &'a str { &s[..1] }

// &self 出现时,输出生命周期默认绑到 &self
impl Cfg {
    fn name(&self) -> &str { &self.name }
    // 等价 fn name<'a>(&'a self) -> &'a str
}

struct Cfg { name: String }

编译器按 3 条省略规则自动补全:每个入参引用给一个独立生命周期;只有一个入参引用时输出取该生命周期;有 &self / &mut self 时输出取 self 的生命周期。规则推不出来时才手写标注。

'static 生命周期

// 字符串字面量天生 &'static str
let s: &'static str = "I live forever";

// 函数签名要求 'static 时 = 数据不能借用栈
fn spawn_log(msg: &'static str) {
    std::thread::spawn(move || println!("{msg}"));
}
spawn_log(s);                       // ✅
// spawn_log(&String::from("x"));   // ❌ 不是 'static

'static 表示引用在整个程序运行期间都有效。字符串字面量就是 &'static str。thread::spawn 这种 API 需要 'static,是为了让派生的线程不会比它捕获的数据活得更久。

持有引用的结构体

struct Excerpt<'a> {
    part: &'a str,
}

impl<'a> Excerpt<'a> {
    fn part(&self) -> &str { self.part }
}

let book = String::from("Call me Ishmael. Some years ago...");
let first = book.split('.').next().unwrap();
let e = Excerpt { part: first };
println!("{}", e.part());           // e 不能比 book 活得久

存了引用的结构体必须声明生命周期参数,让编译器知道它不能比指向的数据活得更久。很多时候更好的选择是直接拥有数据(String、Vec<T>),省去生命周期来回折腾。

多个生命周期

fn split_first<'a, 'b>(s: &'a str, sep: &'b str) -> &'a str {
    s.split(sep).next().unwrap_or(s)
}

let owner = String::from("a,b,c");
let head;
{
    let sep = String::from(",");
    head = split_first(&owner, &sep);   // 返回值绑在 owner 上
}                                       // sep 提前 drop 不影响
println!("{head}");

两个入参生命周期独立、只有其中一个跟返回值有关时,给它们不同的生命周期参数。这告诉编译器返回值绑在 owner 上、跟 sep 无关,sep 可以更早 drop。

NLL 非词法生命周期

let mut v = vec![1, 2, 3];

let first = &v[0];      // 借用开始
println!("{first}");    // 借用最后一次使用 → 借用结束

v.push(4);              // ✅ NLL 之后这条不再报错
println!("{v:?}");

从 Rust 2018 起,借用在最后一次使用时结束,而不是等到词法作用域结束。这让大量直觉上"应该能编过"的代码不再需要塞中间作用域 / 块。

泛型生命周期约束 T: 'a

// T 里若含引用,要求那些引用至少活够 'a
struct Holder<'a, T: 'a> {
    item: &'a T,
}

impl<'a, T: std::fmt::Debug + 'a> Holder<'a, T> {
    fn show(&self) { println!("{:?}", self.item) }
}

let n = 42;
let h = Holder { item: &n };
h.show();

T: 'a 表示类型 T 内部的所有引用都活得不短于 'a。结构体存 &'a T 时需要这条约束,编译器才能保证借用的数据不悬垂。多数时候能推断,老代码或复杂场景要显式写。

高阶 trait 约束 for<'a>

// 闭包对任意生命周期的 &str 都成立
fn apply<F>(f: F) -> String
where
    F: for<'a> Fn(&'a str) -> String,
{
    f("hello")
}

let out = apply(|s| s.to_uppercase());
println!("{out}");              // HELLO

for<'a> 是高阶 trait 约束(HRTB):约束对每一个可能的生命周期 'a 都成立,而不是某个特定的。闭包 / 函数参数接收任意生命周期引用时最常见到它。

返回绑定到 &self 的引用

struct Parser { src: String }

impl Parser {
    // 省略规则:输出生命周期默认 = &self 的
    fn first_word(&self) -> &str {
        self.src.split_whitespace().next().unwrap_or("")
    }
}

let p = Parser { src: "rust is fun".into() };
let w = p.first_word();
println!("{w}");                // rust  (w 不能比 p 活得久)

方法收 &self 并返回引用时,省略规则把输出生命周期绑到 self。返回的 &str 借自结构体,所以结构体必须比返回的引用活得久,不用手写标注。

struct 与 enum (13)

struct 定义与 new()

#[derive(Debug, Clone)]
pub struct User {
    pub name: String,
    pub age: u32,
}

impl User {
    pub fn new(name: impl Into<String>, age: u32) -> Self {
        Self { name: name.into(), age }
    }
}

let u = User::new("li", 18);
println!("{u:?}");

惯例:构造器写成静态方法 new。参数用 impl Into<String> 让调用方可以传 &str、String 或任何能转的类型。Self 指代当前类型。

元组结构体与单元结构体

// 元组结构体:字段没名字,靠位置
struct Wrapping(i32);
struct Rgb(u8, u8, u8);

let w = Wrapping(42);
println!("{}", w.0);
let red = Rgb(255, 0, 0);
println!("{} {} {}", red.0, red.1, red.2);

// 单元结构体:没字段,常作为 trait 占位 / marker
struct Sentinel;
let _ = Sentinel;

元组结构体只给类型起名、不给字段起名,newtype 模式常用。单元结构体连字段都没有,常用作 trait 标记或零尺寸哨兵。

带数据的 enum

enum Shape {
    Circle(f64),                    // r
    Rect { w: f64, h: f64 },        // 命名字段
    Empty,
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rect { w, h } => w * h,
        Shape::Empty => 0.0,
    }
}

println!("{}", area(&Shape::Circle(1.0)));

Rust 的 enum 是和类型:每个变体可以带不同形状的数据(元组、命名结构体或没有)。它表达互斥状态(要么这个要么那个),并通过 match 强制穷尽处理。

Option<T>,Rust 里没有 null

fn find(slice: &[i32], target: i32) -> Option<usize> {
    slice.iter().position(|&x| x == target)
}

let v = vec![10, 20, 30];
match find(&v, 20) {
    Some(i) => println!("at {i}"),
    None    => println!("not found"),
}

// 链式 unwrap_or / map / and_then
let n: i32 = find(&v, 99).map(|i| i as i32).unwrap_or(-1);
println!("{n}");

Option<T> = Some(T) | None 替代 null。编译器强制你处理 None,不可能再有空指针异常。map / and_then / unwrap_or 这些组合子可以优雅地串起来。

Result<T, E>

fn parse_age(s: &str) -> Result<u32, std::num::ParseIntError> {
    s.trim().parse::<u32>()
}

match parse_age(" 18 ") {
    Ok(n)  => println!("age = {n}"),
    Err(e) => println!("bad: {e}"),
}

Result<T, E> = Ok(T) | Err(E) 是 Rust 返回可恢复错误的方式。编译器强制你处理 Err 分支或者用 ? 往上抛,不存在静默抛异常。

#[derive(...)] 常用派生

#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct Tag {
    name: String,
    count: u32,
}

let t = Tag::default();             // Default → 全零 / 空
let t2 = t.clone();
assert_eq!(t, t2);                  // PartialEq + Eq
println!("{t:?}");                  // Debug
// Hash 让 Tag 能当 HashSet / HashMap 的键

derive 在所有字段都支持时自动实现常用 trait。Debug 给 {:?} 打印用,Clone 给 .clone() 用,PartialEq / Eq 给 ==,Hash 让类型能做 HashMap 的键,Default 给 ::default()。

结构体更新语法

#[derive(Debug, Clone)]
struct Config {
    host: String,
    port: u16,
    tls: bool,
}

let base = Config { host: "localhost".into(), port: 80, tls: false };
let prod = Config {
    host: "api.toolora.com".into(),
    tls: true,
    ..base                          // 剩下的字段从 base 拿
};
println!("{prod:?}");

..base 把剩下的字段从另一个值拿过来。注意:被拿过来的字段如果不是 Copy(比如 String),就把 base move 走了,base 之后就不完整可用了。

impl 块,方法与关联函数

struct Counter { n: u32 }

impl Counter {
    pub fn new() -> Self { Self { n: 0 } }   // 关联函数
    pub fn incr(&mut self) { self.n += 1 }   // 方法 &mut self
    pub fn value(&self) -> u32 { self.n }    // 方法 &self
    pub fn into_inner(self) -> u32 { self.n } // 消费 self
}

let mut c = Counter::new();
c.incr();
c.incr();
println!("{}", c.value());
let n = c.into_inner();             // c 之后不能再用

impl 块里没 self 的 fn 是关联函数(用 Type::name 调用);&self / &mut self / self 是方法接收者。接收 self 的方法会消费整个值,之后这个值就不能再用了。

enum 判别值与 as 转换

#[derive(Debug, Clone, Copy)]
enum Status {
    Active = 1,
    Paused = 2,
    Closed = 9,
}

let s = Status::Paused;
let code = s as i32;            // 无字段的 enum 可 as 成整数
println!("{code}");             // 2

无字段(C 风格)的 enum 可以给每个变体指定整数判别值,并用 as 转成整数。序列化成状态码很方便。注意:只有无字段 enum 才支持 as 转换。

matches! 宏

#[derive(Debug)]
enum Token { Num(i32), Plus, Minus }

let t = Token::Num(5);

// 只想知道"是不是某种形状",不用写完整 match
let is_num = matches!(t, Token::Num(_));
let is_op  = matches!(t, Token::Plus | Token::Minus);
println!("{is_num} {is_op}");   // true false

matches!(表达式, 模式) 返回 bool,表达式匹配模式(可带守卫)就是 true。只想判断"是不是某种形状"时,比写完整 match 干净得多。

Option 组合子 map / and_then / filter

let s = Some("42");

let n: Option<i32> = s
    .and_then(|x| x.parse().ok())   // Option<i32>,解析失败 → None
    .filter(|&n| n > 0)             // 不满足 → None
    .map(|n| n * 10);               // 包在 Some 里变换

println!("{n:?}");                  // Some(420)
println!("{:?}", None::<i32>.unwrap_or(0));

不解包就能链式处理 Option:map 变换内部值,and_then(flatMap)接下一个返回 Option 的步骤,filter 把不满足条件的 Some 变 None,ok_or 把 None 转成 Err。省掉层层嵌套的 match。

Result 组合子 map_err / and_then / ok

fn parse(s: &str) -> Result<i32, String> {
    s.parse::<i32>()
        .map_err(|e| format!("parse: {e}"))   // 转错误类型
        .and_then(|n| if n >= 0 { Ok(n) } else { Err("negative".into()) })
}

println!("{:?}", parse("7"));       // Ok(7)
println!("{:?}", parse("-3"));      // Err("negative")
let opt: Option<i32> = parse("x").ok();   // Result → Option,丢弃错误
println!("{opt:?}");

map 变换 Ok,map_err 变换 Err,and_then 接下一个可能失败的步骤,.ok() 把错误丢弃转成 Option,.unwrap_or_else 惰性处理 Err。这些把流程拼成可读的管道,不用嵌套 match。

给自定义类型实现 Iterator

struct Fib { a: u64, b: u64 }

impl Iterator for Fib {
    type Item = u64;
    fn next(&mut self) -> Option<u64> {
        let cur = self.a;
        self.a = self.b;
        self.b = cur + self.b;
        Some(cur)               // 永不结束 → 配 take 用
    }
}

let v: Vec<u64> = Fib { a: 0, b: 1 }.take(8).collect();
println!("{v:?}");              // [0,1,1,2,3,5,8,13]

实现 Iterator 只要定义 type Item 和 next(&mut self) -> Option<Item>。然后整套组合子(map / filter / take ...)就白送给你了。返回 None 结束;这里永不结束,所以配 take(n)。

trait (15)

trait 定义与默认方法

pub trait Greet {
    fn name(&self) -> &str;             // 必须实现
    fn hello(&self) {                   // 默认方法
        println!("hi, {}", self.name());
    }
}

struct Dog;
impl Greet for Dog {
    fn name(&self) -> &str { "Rex" }
}

Dog.hello();

trait 是一组方法契约。给默认方法体后,实现方可以选择覆盖。没有方法体的"必须方法"则每个 impl 都得提供。

impl Trait for Type 与孤儿规则

// 想给 Vec<i32> 实现自己的 trait
pub trait Sum {
    fn sum(&self) -> i32;
}

impl Sum for Vec<i32> {                 // ✅ trait 是本 crate 定义的,所以可以
    fn sum(&self) -> i32 { self.iter().copied().sum() }
}

let v = vec![1, 2, 3];
println!("{}", v.sum());                // 6

孤儿规则:实现 Trait for Type 时,Trait 或 Type 必须有一个是本 crate 定义的。这避免两个 crate 各自给同一对 (Trait, Type) 提供冲突实现。

泛型函数上的 trait 约束

use std::fmt::Display;

fn announce<T: Display>(item: T) {
    println!("breaking news: {item}");
}

// where 子句让多个约束更好读
fn longest<T>(a: T, b: T) -> T
where
    T: PartialOrd,
{
    if a > b { a } else { b }
}

announce("rust 2024");
announce(42);
println!("{}", longest(3, 7));

trait 约束限制 T 能是哪些类型。T: Display + Debug 表示两者都要。where 子句只是约束多时更好读的写法,跟尖括号里写 T: Bound 完全等价。

dyn Trait,动态分发

trait Animal { fn speak(&self); }

struct Dog; impl Animal for Dog { fn speak(&self) { println!("woof") } }
struct Cat; impl Animal for Cat { fn speak(&self) { println!("meow") } }

// Vec 里要存不同具体类型 → 用 trait object
let zoo: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
for a in &zoo { a.speak() }

Box<dyn Trait> 装一个未知大小的值,方法调用通过虚表在运行期分发。需要存"不同具体类型一起"或对外隐藏具体类型时用它。

impl Trait 返回,静态分发

// 调用方拿到一个实现了 Iterator 的具体类型,但不在乎是哪个
fn evens(n: u32) -> impl Iterator<Item = u32> {
    (0..n).filter(|x| x % 2 == 0)
}

for x in evens(10) { print!("{x} ") }   // 0 2 4 6 8

impl Trait 作返回类型表示"某个实现了 Trait 的具体类型,我不告诉你是哪个"。编译器选一个具体类型并单态化,没有虚表。但不同分支不能返回不同具体类型,那种场景要用 Box<dyn>。

From / Into,值转换

struct UserId(u64);

impl From<u64> for UserId {
    fn from(n: u64) -> Self { UserId(n) }
}

let id: UserId = 42_u64.into();         // Into 是 From 反向自动派生
let id2 = UserId::from(42_u64);
println!("{} {}", id.0, id2.0);

只要实现 From<X> for Y,就自动获得 Into<Y> for X。自己代码里优先实现 From,调用方就可以写 x.into() 或 Y::from(x)。函数参数写 impl Into<T> 能接收任何可转换的类型。

Display 与 Debug 的区别

use std::fmt;

#[derive(Debug)]
struct Point { x: i32, y: i32 }

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

let p = Point { x: 1, y: 2 };
println!("{p}");                        // Display:  (1, 2)
println!("{p:?}");                      // Debug:    Point { x: 1, y: 2 }
println!("{p:#?}");                     // 漂亮 Debug

Display({})是给用户看的,要手写。Debug({:?})是给开发者看的快照,多数时候 #[derive(Debug)] 就够。{:#?} 是漂亮打印。

PartialOrd / Ord,可排序类型

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Ver(u32, u32, u32);

let mut v = vec![Ver(1,2,0), Ver(0,9,9), Ver(1,2,1)];
v.sort();                               // 默认升序
println!("{v:?}");

// 自定义键排序
let mut nums = vec![3.0_f64, 1.0, 2.0];
nums.sort_by(|a, b| a.partial_cmp(b).unwrap());

想要全序就 derive PartialOrd + Ord,字段按顺序比较。f32 / f64 只实现 PartialOrd(NaN 不能比较), 确认没 NaN 就用 sort_by 加 partial_cmp().unwrap()。

扩展外部类型,newtype 模式

// 想给 Vec<String> 加方法,但 Vec 和方法 trait 都不是本 crate 的 → 用 newtype 包一层
struct Tags(Vec<String>);

impl Tags {
    fn joined(&self) -> String { self.0.join(", ") }
}

let t = Tags(vec!["a".into(), "b".into(), "c".into()]);
println!("{}", t.joined());             // a, b, c

newtype 就是把已有类型用元组结构体包一层。可以加方法、绕开孤儿规则给外部类型实现外部 trait、还能区分类型,UserId(u64) 和 PostId(u64) 不会混用。

关联类型 vs 泛型参数

// 关联类型:一种类型只能选一个 Item,签名最干净
pub trait Container {
    type Item;
    fn get(&self, i: usize) -> Option<&Self::Item>;
}

// 泛型参数:同一种类型可以为多个 T 实现,灵活但调用方要标注
pub trait Convert<T> {
    fn to(&self) -> T;
}

struct OneShot;
impl Convert<String> for OneShot { fn to(&self) -> String { "s".into() } }
impl Convert<i32>    for OneShot { fn to(&self) -> i32    { 0 } }

let s: String = OneShot.to();
let n: i32    = OneShot.to();
println!("{s} {n}");

关联类型:一个类型只能实现一次、签名最干净(如 Iterator::Item)。trait 上的泛型参数:同一类型可以为多个 T 实现(From<i32>、From<u64>)。除非真要多实现,否则选关联类型。

TryFrom / TryInto,可失败转换

use std::convert::TryFrom;

struct Age(u8);

impl TryFrom<i32> for Age {
    type Error = String;
    fn try_from(n: i32) -> Result<Self, String> {
        if (0..=120).contains(&n) { Ok(Age(n as u8)) }
        else { Err(format!("age out of range: {n}")) }
    }
}

println!("{:?}", Age::try_from(30).map(|a| a.0));   // Ok(30)
println!("{:?}", Age::try_from(999).map(|a| a.0));  // Err(...)

TryFrom<X> 是 From<X> 的可失败版:try_from 返回 Result<Self, Error>。实现它同时白送 TryInto,是"转换时顺带校验"(如 i32 转有界 Age)的地道写法。

Deref,智能指针透明化

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T { &self.0 }
}

let b = MyBox(String::from("hi"));
println!("{}", b.len());        // deref coercion: &MyBox<String> → &String → &str
fn takes(s: &str) { println!("{s}") }
takes(&b);                      // 自动 deref 到 &str

实现 Deref 后可以直接调内部类型的方法,并触发解引用强转(&MyBox<String> → &str)。Box / Rc / String 的透明感就靠它。别拿它假装继承来滥用。

运算符重载,Add

use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct V2 { x: f64, y: f64 }

impl Add for V2 {
    type Output = V2;
    fn add(self, o: V2) -> V2 {
        V2 { x: self.x + o.x, y: self.y + o.y }
    }
}

let r = V2 { x: 1.0, y: 2.0 } + V2 { x: 3.0, y: 4.0 };
println!("{r:?}");              // V2 { x: 4.0, y: 6.0 }

运算符就是 std::ops 里的 trait:Add(+)、Sub(-)、Mul(*)、Index([])等。给类型实现 Add(带 Output 关联类型)就能用 +。其它算术运算符写法一样。

一揽子实现 (blanket impl)

trait Summary {
    fn summary(&self) -> String;
}

// 给所有实现了 Display 的类型一次性实现 Summary
impl<T: std::fmt::Display> Summary for T {
    fn summary(&self) -> String {
        format!("[{self}]")
    }
}

println!("{}", 42.summary());       // [42]
println!("{}", "hi".summary());     // [hi]

一揽子实现 impl<T: Bound> Trait for T 一次性给所有满足约束的类型实现你的 trait。标准库 ToString 就是这样给所有 Display 类型实现的。很强,但可能引发一致性冲突。

超 trait (supertrait)

use std::fmt::Display;

// 实现 Named 的类型必须也实现 Display
trait Named: Display {
    fn name(&self) -> String {
        format!("name is {self}")   // 可以直接用 Display 的能力
    }
}

struct Cat;
impl Display for Cat {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "Cat")
    }
}
impl Named for Cat {}

println!("{}", Cat.name());     // name is Cat

trait Named: Display 把 Display 声明为超 trait,每个实现 Named 的类型都必须也实现 Display,且 Named 的默认方法可以依赖 Display 的能力。用于要求一个 trait 所依赖的能力。

泛型 (9)

泛型函数

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut max = list[0];
    for &x in list {
        if x > max { max = x }
    }
    max
}

println!("{}", largest(&[10, 1, 50, 20]));     // 50
println!("{}", largest(&[3.5, 1.0, 7.2]));     // 7.2

泛型函数用尖括号 <T>。trait 约束(T: PartialOrd)限制函数体能对 T 做哪些操作。单态化在编译期为每个具体 T 生成专用版本,运行期零开销。

泛型 struct 与 impl

struct Pair<A, B> { a: A, b: B }

impl<A, B> Pair<A, B> {
    fn swap(self) -> Pair<B, A> { Pair { a: self.b, b: self.a } }
}

// 给特定具化版本加方法
impl Pair<i32, i32> {
    fn sum(&self) -> i32 { self.a + self.b }
}

let p = Pair { a: 1, b: "two" };
let p2 = p.swap();
println!("{} {}", p2.a, p2.b);
println!("{}", Pair { a: 1, b: 2 }.sum());

struct 的泛型参数同时出现在类型和每个 impl 块上。可以为完全具化版本(如 Pair<i32, i32>)单独写 impl,给该具化加方法。

多 trait 约束 +

use std::fmt::{Debug, Display};

fn show<T: Debug + Display>(x: T) {
    println!("{x} / {x:?}");
}

// where 子句更好读
fn pair<A, B>(a: A, b: B) -> (A, B)
where
    A: Clone + Debug,
    B: Clone + Debug,
{
    (a.clone(), b.clone())
}

show(42);                       // 42 / 42
println!("{:?}", pair("x", 7));

用 + 组合多个约束。类型参数多、每个参数约束也多时用 where 子句,语义一致但好读得多。

const 泛型,把数组长度当参数

fn first<const N: usize>(arr: [i32; N]) -> Option<i32> {
    arr.first().copied()
}

println!("{:?}", first([1, 2, 3]));         // Some(1)
println!("{:?}", first::<0>([]));           // None

// 真实用例:固定大小缓冲区
struct RingBuf<T, const N: usize> { buf: [Option<T>; N] }

const 泛型让数组长度等编译期常量也能做类型参数。一个函数就能处理任意 [T; N],运行期零分配。原始类型的 const 参数从 1.51 起稳定。

单态化,零开销泛型

fn id<T>(x: T) -> T { x }

let a = id(1_i32);
let b = id("hi");
// 编译后等价于两个独立函数:id_i32 / id_str
// 调用方完全没有运行期开销

每个泛型实例化得到一份独立编译的函数(单态化)。没有虚表、没有装箱,性能等同手写的具化版本,代价是二进制更大。

turbofish ::<T>

let n: u32 = "42".parse().unwrap();             // 上下文够 → 不用 turbofish
let m = "42".parse::<u32>().unwrap();           // turbofish 显式指定
let v = (0..5).collect::<Vec<_>>();             // collect 常用 turbofish

println!("{n} {m} {v:?}");

编译器推不出泛型类型时(.collect() 是经典场景),用 turbofish ::<T> 显式指定。初见怪怪的,但这就是官方写法。

impl Trait 作参数

// 等价于 fn print_all<I: IntoIterator<Item = i32>>(it: I)
fn print_all(it: impl IntoIterator<Item = i32>) {
    for x in it { print!("{x} ") }
}

print_all(vec![1, 2, 3]);
print_all(0..3);
print_all([10, 20]);

impl Trait 作参数是匿名泛型参数的简写:fn f(x: impl Trait) 等价 fn f<T: Trait>(x: T)。单处使用的简单约束更干净;需要在别处指名类型时用显式 <T>。

幻影类型 PhantomData

use std::marker::PhantomData;

struct Meters;
struct Feet;

struct Length<Unit> {
    value: f64,
    _unit: PhantomData<Unit>,
}

impl<Unit> Length<Unit> {
    fn new(v: f64) -> Self { Length { value: v, _unit: PhantomData } }
}

let m: Length<Meters> = Length::new(3.0);
let f: Length<Feet> = Length::new(9.8);
// m 与 f 是不同类型,编译期不会混用
println!("{} {}", m.value, f.value);

PhantomData<T> 是零尺寸标记,让类型对 T 泛型却不实际存 T。用于类型级标签(单位、状态),让编译器把 Length<Meters> 和 Length<Feet> 区分开,运行期零开销。

泛型默认类型参数

use std::ops::Add;

// Add 的真实签名带默认:trait Add<Rhs = Self>
#[derive(Debug)]
struct Millis(u64);

impl Add<u64> for Millis {          // 右操作数是 u64,不是 Self
    type Output = Millis;
    fn add(self, rhs: u64) -> Millis { Millis(self.0 + rhs) }
}

println!("{:?}", Millis(100) + 50); // Millis(150)

泛型参数可以有默认:trait Add<Rhs = Self>。所以 Millis + Millis 用默认的 Self,但你也能 impl Add<u64> 让 Millis + u64 成立。默认参数让一个 trait 同时覆盖常见和定制场景。

集合 (12)

Vec<T> 基础

let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);

let v2 = vec![10, 20, 30];          // vec! 宏
let v3: Vec<i32> = (0..5).collect();

println!("{} {} {}", v.len(), v[0], v.last().unwrap());
let popped = v.pop();               // Option<i32>
println!("{popped:?} {v:?} {v2:?} {v3:?}");

Vec<T> 是堆上可增长的动态数组,最常用的容器。.push / .pop / .len / .iter 覆盖大多数需求。v[i] 越界会 panic;.get(i) 返回 Option<&T> 安全访问。

Vec 的 iter / iter_mut / into_iter

let v = vec![1, 2, 3];

for x in v.iter()     { print!("{x} ") }    // &i32     (v 还能用)
for x in &v           { print!("{x} ") }    // 同 .iter()
let mut w = v.clone();
for x in w.iter_mut() { *x *= 10 }          // &mut i32 (原地修改)
for x in v.into_iter(){ print!("{x} ") }    // i32      (v 被消费)

.iter() 返回 &T(集合还能用),.iter_mut() 返回 &mut T,.into_iter() 返回 T 并消费集合。for x in &v 等价于 .iter(),for x in v 等价于 .into_iter()。

HashMap<K, V>

use std::collections::HashMap;

let mut m: HashMap<String, u32> = HashMap::new();
m.insert("apple".into(), 3);
m.insert("pear".into(), 5);

// 取值
if let Some(n) = m.get("apple") { println!("{n}") }

// 不存在则插入默认值
*m.entry("apple".into()).or_insert(0) += 1;

// 遍历
for (k, v) in &m { println!("{k}: {v}") }

HashMap 是标准哈希表。.entry(k).or_insert(v)"不存在就插入,返回 &mut V"是地道写法,做计数最合适。遍历顺序不固定(默认每进程随机化以防 HashDoS)。

HashSet,唯一性

use std::collections::HashSet;

let words = ["a", "b", "a", "c", "b"];
let uniq: HashSet<&str> = words.iter().copied().collect();
println!("{}", uniq.len());                 // 3

let a: HashSet<i32> = [1, 2, 3].into();
let b: HashSet<i32> = [2, 3, 4].into();
let inter: HashSet<i32> = a.intersection(&b).copied().collect();
println!("{inter:?}");                      // {2, 3}

HashSet<T> 是没有值的哈希表,用于去重和集合运算(并、交、差)。用 .collect::<HashSet<_>>() 从迭代器构造一次性 O(n) 去重。

BTreeMap,按键排序

use std::collections::BTreeMap;

let mut m = BTreeMap::new();
m.insert(3, "three");
m.insert(1, "one");
m.insert(2, "two");

for (k, v) in &m { println!("{k} {v}") }    // 1 2 3 顺序

// 范围查询
for (k, v) in m.range(2..) { println!("≥2: {k} {v}") }

BTreeMap 按键排序,比 HashMap 略慢,但能有序遍历、支持范围查询。需要稳定顺序或区间查找时选它。

String 常用操作

let mut s = String::from("hello");
s.push(' ');
s.push_str("world");

let n = s.len();                    // 字节长度
let chars = s.chars().count();      // 字符数(多字节正确)
let upper = s.to_uppercase();
let parts: Vec<&str> = s.split(' ').collect();
let joined = parts.join(",");

println!("{n} {chars} {upper} {joined}");

String 的 len() 返回字节数不是字符数,字符数用 .chars().count()。常用:.push / .push_str / .split / .replace / .to_uppercase / .trim / .starts_with。切片 s[0..3] 必须落在字符边界上,否则 panic。

VecDeque,双端队列

use std::collections::VecDeque;

let mut q: VecDeque<i32> = VecDeque::new();
q.push_back(1);
q.push_back(2);
q.push_front(0);                    // [0, 1, 2]

assert_eq!(q.pop_front(), Some(0));
assert_eq!(q.pop_back(),  Some(2));
println!("{q:?}");                  // [1]

VecDeque 是两端 O(1) push / pop 的环形缓冲。用于 FIFO 队列(BFS、任务队列)或滑动窗口,Vec::remove(0) 是 O(n),用 VecDeque 代替。

sort / sort_by / sort_by_key

let mut nums = vec![3, 1, 4, 1, 5, 9, 2, 6];
nums.sort();                                // 升序
nums.sort_by(|a, b| b.cmp(a));              // 降序

let mut words = vec!["apple", "kiwi", "banana"];
words.sort_by_key(|w| w.len());             // 按长度
println!("{nums:?} {words:?}");

// 稳定 vs 非稳定
nums.sort_unstable();                       // 更快,不保证相等元素顺序

.sort() 稳定 O(n log n);.sort_unstable() 更快但不保证相等元素相对顺序。自定义比较用 .sort_by(|a, b| ...),按提取键排序用 .sort_by_key(|x| ...)。

Vec 的 retain / dedup / drain

let mut v = vec![1, 1, 2, 3, 3, 3, 4];

v.dedup();                      // 去掉相邻重复 → [1,2,3,4]
v.retain(|&x| x % 2 == 0);      // 原地保留偶数 → [2,4]

let mut w = vec![10, 20, 30, 40];
let mid: Vec<i32> = w.drain(1..3).collect();  // 取走索引 1..3
println!("{v:?} {mid:?} {w:?}");              // [2,4] [20,30] [10,40]

retain(|x| ...) 原地保留满足条件的元素(O(n),不新分配)。dedup() 去相邻重复(要全局去重先 sort)。drain(范围) 移除并产出一段子区间,剩下的元素前移补齐。

HashMap entry 的 or_insert_with

use std::collections::HashMap;

let text = "a b a c b a";
let mut counts: HashMap<&str, u32> = HashMap::new();
for w in text.split_whitespace() {
    *counts.entry(w).or_insert(0) += 1;
}
println!("{:?}", counts.get("a"));   // Some(3)

// 值是 Vec 时用 or_insert_with 避免每次都构造空 Vec
let mut groups: HashMap<char, Vec<&str>> = HashMap::new();
for w in ["apple", "avocado", "berry"] {
    groups.entry(w.chars().next().unwrap())
          .or_insert_with(Vec::new)
          .push(w);
}
println!("{:?}", groups.get(&'a'));

entry(k).or_insert(v) 返回 &mut V,不存在就插 v,做计数最合适。默认值构造代价大(如 Vec::new)时用 or_insert_with(|| ...),只在 miss 时才构造。

切片模式与 windows / chunks

let v = [1, 2, 3, 4, 5];

// 切片模式解构
if let [first, .., last] = v {
    println!("{first} {last}");      // 1 5
}

// 滑动窗口与定长分块
for w in v.windows(2) { print!("{w:?} ") }   // [1,2] [2,3] ...
println!();
for c in v.chunks(2) { print!("{c:?} ") }    // [1,2] [3,4] [5]

切片模式 [first, .., last] 用 .. 绑定中间剩余部分解构数组 / 切片。windows(n) 产出长度 n 的重叠子切片(算差分 / 配对好用);chunks(n) 产出不重叠的定长块(最后一块可能更短)。

BinaryHeap,优先队列

use std::collections::BinaryHeap;
use std::cmp::Reverse;

let mut heap = BinaryHeap::new();
heap.push(3);
heap.push(1);
heap.push(4);
println!("{:?}", heap.pop());        // Some(4)  默认大顶堆

// 用 Reverse 变小顶堆
let mut min = BinaryHeap::new();
for n in [3, 1, 4, 1, 5] { min.push(Reverse(n)) }
println!("{:?}", min.pop());         // Some(Reverse(1))

BinaryHeap 是大顶堆:push 是 O(log n),pop 返回最大值也是 O(log n)。把元素包进 std::cmp::Reverse 就得到小顶堆。Dijkstra、top-k、任务调度都用它。

错误处理 (12)

? 运算符,遇 Err 提前返回

use std::fs;
use std::io;

fn read_first_line(path: &str) -> io::Result<String> {
    let s = fs::read_to_string(path)?;      // Err -> 立刻 return Err
    Ok(s.lines().next().unwrap_or("").to_string())
}

match read_first_line("/etc/hostname") {
    Ok(l)  => println!("first: {l}"),
    Err(e) => println!("err: {e}"),
}

? 运算符 Ok 就解包,Err 就立刻返回给调用方。错误类型必须能 From 转换为函数声明的错误类型。一个字符替代 8 行 match 模板。

Box<dyn Error>,快速错误类型

use std::error::Error;
use std::fs;

fn main() -> Result<(), Box<dyn Error>> {
    let s = fs::read_to_string("Cargo.toml")?;
    let n: usize = s.lines().count();
    println!("{n} lines");
    Ok(())
}

Box<dyn Error> 是"任意错误"懒人类型,小二进制和原型阶段最合适。用动态分发,擦除具体类型。库要发布的话,用命名 enum 错误或 anyhow::Error / eyre::Report。

自定义错误 enum

use std::fmt;

#[derive(Debug)]
pub enum DbError {
    NotFound,
    Conflict(String),
    Io(std::io::Error),
}

impl fmt::Display for DbError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            DbError::NotFound       => write!(f, "row not found"),
            DbError::Conflict(s)    => write!(f, "conflict: {s}"),
            DbError::Io(e)          => write!(f, "io: {e}"),
        }
    }
}

impl std::error::Error for DbError {}

// 让 ? 能从 io::Error 自动转
impl From<std::io::Error> for DbError {
    fn from(e: std::io::Error) -> Self { DbError::Io(e) }
}

库代码应该定义命名 enum,让调用方能 match 不同变体。实现 Display + Error 让打印好看,写 From<其他错误> 让 ? 能自动转。thiserror crate 用 derive 一次性生成这些。

panic!,不可恢复

fn divide(a: i32, b: i32) -> i32 {
    if b == 0 { panic!("divide by zero: {a}/{b}") }
    a / b
}

// .unwrap() 与 .expect() 在 None / Err 时 panic
let x: i32 = "42".parse().expect("must be an integer");
println!("{x}");

panic! 终止当前线程(如配置成 abort 则整个程序),RUST_BACKTRACE=1 时打栈追踪。只用于真正的 bug 和被破坏的不变量,预期错误用 Result。

unwrap_or 系列

let n: i32 = "x".parse().unwrap_or(-1);                 // 失败给 -1
let m: i32 = "x".parse().unwrap_or_else(|_| 0);         // 失败时再计算
let s: String = None::<String>.unwrap_or_default();      // 类型的 Default
println!("{n} {m} {s:?}");

unwrap_or(x):None / Err 时用 x。unwrap_or_else(|e| ...):同样意思,但回退值是惰性计算还能看错误。unwrap_or_default():用类型的 Default::default()。有合理 fallback 时优先用这几个,少用 .unwrap()。

main 返回 Result 后用 ?

use std::fs;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let s = fs::read_to_string("Cargo.toml")?;
    println!("{} bytes", s.len());
    Ok(())
}

main 可以返回 Result<(), E>。这样 main 里直接用 ? 就行;遇 Err 程序以退出码 1 退出并用 Debug 打印错误。写命令行工具特别好用。

? 用 From 跨类型自动转换

// ? 触发 From 自动转换:io::Error -> MyError
#[derive(Debug)]
struct MyError(String);

impl From<std::io::Error> for MyError {
    fn from(e: std::io::Error) -> Self { MyError(e.to_string()) }
}

fn run() -> Result<(), MyError> {
    let _ = std::fs::read_to_string("Cargo.toml")?;   // ? 自动 .into() 转
    Ok(())
}

当 ? 遇到 E1 类型的错误而函数返回 Result<_, E2> 时,它会调 E1.into(),需要存在 impl From<E1> for E2。这就是分层错误 enum 如何把 IO / 解析 / 网络错误统一往上传播的。

永不返回类型 !,发散函数

fn forever() -> ! { loop {} }
fn die(msg: &str) -> ! { panic!("{msg}") }

// ! 能强转成任何类型,所以下面 match 的分支类型一致
let n: i32 = match "x".parse::<i32>() {
    Ok(v) => v,
    Err(_) => die("bad number"),    // 永不返回,类型 = !
};
println!("{n}");

! 是"永不返回"类型,给永远不返回的函数用(死循环、panic!、exit)。因为 ! 能强转成任何类型,可以在 match 分支里随便用 die() / panic!() 不会破坏分支类型一致。

ok_or / ok_or_else,Option 转 Result

fn lookup(map: &std::collections::HashMap<&str, i32>, k: &str)
    -> Result<i32, String>
{
    map.get(k)
        .copied()
        .ok_or_else(|| format!("key not found: {k}"))   // None → Err
}

let mut m = std::collections::HashMap::new();
m.insert("x", 1);
println!("{:?}", lookup(&m, "x"));   // Ok(1)
println!("{:?}", lookup(&m, "y"));   // Err("key not found: y")

ok_or(err) / ok_or_else(|| err) 把 Option<T> 转成 Result<T, E>,Some(v) 转 Ok(v),None 转 Err(err)。构造错误代价大时用 _else 版本,只在 None 路径才执行。这样 ? 就能把"缺值"往上传播。

用 .map_err 加错误上下文

use std::fs;

fn load(path: &str) -> Result<String, String> {
    fs::read_to_string(path)
        .map_err(|e| format!("failed to read {path}: {e}"))
}

println!("{:?}", load("/no/such/file").err());
// Some("failed to read /no/such/file: No such file or directory ...")

map_err 在往上传播前给底层错误加上下文(哪个文件、哪一步)。真实项目里 anyhow 的 .context("...") 或 thiserror 的 #[from] 更顺手,但 map_err 是只靠标准库的基础写法。

if let Err,只处理错误分支

fn write_log(line: &str) -> std::io::Result<()> {
    use std::io::Write;
    let mut f = std::fs::OpenOptions::new()
        .create(true).append(true).open("app.log")?;
    writeln!(f, "{line}")
}

if let Err(e) = write_log("started") {
    eprintln!("log failed: {e}");   // 失败只记一笔,不中断主流程
}

只关心失败分支时(比如尽力而为的日志),if let Err(e) = ... 处理它,不用写完整 match 也不中断程序。常用于"发出去就不管"的副作用操作。

assert! 与 debug_assert! 校验不变量

fn split_at(s: &str, mid: usize) -> (&str, &str) {
    assert!(mid <= s.len(), "mid {mid} out of bounds (len {})", s.len());
    debug_assert!(s.is_char_boundary(mid), "not a char boundary");
    (&s[..mid], &s[mid..])
}

println!("{:?}", split_at("hello", 2));   // ("he", "llo")

assert! 始终运行,条件为假就 panic,用来强制真正的不变量、文档化前置条件。debug_assert! 在 release 构建里被编成空操作,所以把昂贵检查放它那。两者都能带自定义消息。

闭包 (8)

闭包基础

let add = |a: i32, b: i32| -> i32 { a + b };
let inc = |x| x + 1;                // 类型推断

println!("{}", add(2, 3));
println!("{}", inc(10));

let multiline = |s: &str| {
    let upper = s.to_uppercase();
    upper + "!"
};
println!("{}", multiline("hi"));

闭包用 |参数| 函数体 语法。参数和返回类型通常被推断(一个调用点)。多行函数体要加大括号。闭包可以捕获外层作用域的变量。

Fn / FnMut / FnOnce

fn call_fn(f: impl Fn() -> i32) -> i32 { f() + f() }
fn call_mut(mut f: impl FnMut()) { f(); f() }
fn call_once(f: impl FnOnce() -> String) -> String { f() }

let x = 10;
call_fn(|| x);                          // 只读捕获 → Fn

let mut n = 0;
call_mut(|| { n += 1; });               // 可变捕获 → FnMut

let s = String::from("owned");
let out = call_once(move || s);          // 消费捕获 → FnOnce
println!("{out}");

闭包有三个 trait:Fn(可多次调用、只读捕获)、FnMut(多次调用、修改捕获)、FnOnce(消费捕获,只能调一次)。Fn 最严,每个 Fn 同时也是 FnMut 和 FnOnce。

move 关键字,闭包接管捕获的所有权

use std::thread;

let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
    println!("{:?}", data);             // data 被 move 进闭包
});
// println!("{:?}", data);              // ❌ 已被 move
handle.join().unwrap();

没有 move 时闭包借用外层变量。加 move 后捕获被 move 进闭包内部。派生线程、从函数返回闭包等"闭包活得比外层作用域久"的场景必须 move。

返回闭包,Box<dyn Fn>

fn make_adder(n: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |x| x + n)
}

let add5 = make_adder(5);
println!("{}", add5(10));               // 15

// 静态分发:impl Fn (同一个具体闭包类型)
fn make_inc() -> impl Fn(i32) -> i32 { |x| x + 1 }

每个闭包是自己独立的匿名类型且大小不固定。返回闭包要么装箱(Box<dyn Fn(...)>)动态分发,要么用 impl Fn(...) 返回单个匿名具体类型。不同分支返回不同闭包就必须用 Box。

闭包配迭代器组合子

let nums = vec![1, 2, 3, 4, 5];

let sum_sq: i32 = nums.iter()
    .filter(|&&x| x % 2 == 0)           // 偶数
    .map(|&x| x * x)                    // 平方
    .sum();                             // 累加
println!("{sum_sq}");                   // 4 + 16 = 20

let first_big = nums.iter().find(|&&x| x > 3);  // Option<&i32>
println!("{first_big:?}");              // Some(4)

闭包配迭代器组合子最香:.filter / .map / .fold / .find / .any / .all。编译器激进内联,链式调用通常和手写循环编出来的机器码一样紧凑。

闭包按引用 vs 按 move 捕获

let data = vec![1, 2, 3];

// 默认按引用捕获:闭包活在当前作用域内才行
let print_ref = || println!("{data:?}");
print_ref();
println!("{}", data.len());     // ✅ data 还能用,只是被借了

// move:捕获所有权,闭包可送去别处(线程 / 返回)
let owned = move || println!("{data:?}");
owned();
// println!("{}", data.len());  // ❌ 已被 move

闭包按它需要的最宽松方式捕获:先 &,再 &mut,最后按值。move 强制按值捕获,跟需求无关,闭包要活得比当前作用域久时(线程、返回闭包)必须用它。

函数指针 vs 闭包

fn double(x: i32) -> i32 { x * 2 }

// 不捕获环境的闭包可强转成函数指针 fn(i32) -> i32
let f: fn(i32) -> i32 = double;
let g: fn(i32) -> i32 = |x| x + 1;   // ✅ 无捕获,可以
println!("{} {}", f(5), g(5));       // 10 6

// map 既能收函数指针也能收闭包
let v: Vec<i32> = vec![1, 2, 3].into_iter().map(double).collect();
println!("{v:?}");

普通函数(以及无捕获闭包)能强转成函数指针类型 fn(A) -> B。有捕获的闭包不行,要用 Fn / FnMut / FnOnce。需要"类函数值"时,函数指针是最小、可 Copy 的选项。

闭包做结构体字段

struct Button<F: Fn()> {
    label: String,
    on_click: F,
}

impl<F: Fn()> Button<F> {
    fn click(&self) {
        println!("clicked {}", self.label);
        (self.on_click)();      // 调用字段闭包要加括号
    }
}

let b = Button { label: "ok".into(), on_click: || println!("handled") };
b.click();

闭包存进结构体可用泛型 F: Fn() 字段(单态化、不装箱)或 Box<dyn Fn()> 字段(统一类型、动态分发)。调用要加括号:(self.字段)()。泛型更快;类型多变时装箱更省事。

迭代器 (14)

map / filter / collect

let nums = vec![1, 2, 3, 4, 5];

let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
let big: Vec<i32> = nums.iter().copied().filter(|x| *x > 2).collect();
let evens_sq: Vec<i32> = nums.iter()
    .filter(|x| **x % 2 == 0)
    .map(|x| x * x)
    .collect();

println!("{doubled:?} {big:?} {evens_sq:?}");

迭代器是惰性的:map / filter 只是搭管道;.collect()(或 .for_each / .sum / .count)才真正驱动执行。目标类型推不出来时给 collect 加 turbofish:.collect::<Vec<_>>()。

fold,从左累计

let words = vec!["rust", "is", "fast"];

let joined: String = words.iter().fold(String::new(), |mut acc, w| {
    if !acc.is_empty() { acc.push(' ') }
    acc.push_str(w);
    acc
});
println!("{joined}");                   // rust is fast

let sum = (1..=100).fold(0, |a, b| a + b);
println!("{sum}");                      // 5050

fold(初值, |acc, x| ...) 是通用 reduce:从初值开始,把 acc 在每个元素间穿过去。.sum() / .product() / .min() 是特化版。.reduce(|a, b| ...) 从第一个元素开始(返回 Option)。

enumerate / zip

let names = ["Ada", "Bob", "Cy"];
for (i, n) in names.iter().enumerate() {
    println!("{i}: {n}");
}

let ages = [30, 25, 40];
for (n, a) in names.iter().zip(ages.iter()) {
    println!("{n} is {a}");
}

enumerate() 产出 (索引, 元素)。zip(其他) 成对产出,到较短那个用完为止。组合起来就能"带下标多迭代器循环",完全不用手动下标。

take / skip / chain / rev

let v: Vec<_> = (1..).take(5).collect();        // [1,2,3,4,5]
let w: Vec<_> = (1..10).skip(7).collect();      // [8, 9]
let c: Vec<_> = (1..=3).chain(10..=12).collect();// [1,2,3,10,11,12]
let r: Vec<_> = (1..=5).rev().collect();         // [5,4,3,2,1]
println!("{v:?} {w:?} {c:?} {r:?}");

take(n) 取前 n 个。skip(n) 跳过前 n 个。chain(其他) 把两个迭代器首尾相连。rev() 反转一个 DoubleEndedIterator。都不分配内存,随便组合。

any / all / find

let v = vec![1, 2, 3, 4, 5];

let has_neg  = v.iter().any(|x| *x < 0);            // false
let all_pos  = v.iter().all(|x| *x > 0);            // true
let first_gt = v.iter().find(|x| **x > 3);          // Some(&4)
let pos      = v.iter().position(|x| *x == 3);      // Some(2)

println!("{has_neg} {all_pos} {first_gt:?} {pos:?}");

any / all / find / position 都短路,一旦得出答案就停止迭代。在无限或巨型迭代器上找第一个匹配时几乎零成本。

collect 收集到 HashMap / String

use std::collections::HashMap;

let pairs = vec![("a", 1), ("b", 2), ("c", 3)];
let map: HashMap<&str, i32> = pairs.into_iter().collect();
println!("{}", map["b"]);

let chars = vec!['r', 'u', 's', 't'];
let s: String = chars.into_iter().collect();
println!("{s}");                        // rust

collect() 可以收集到任何实现了 FromIterator 的类型:Vec<T>、HashMap<K, V>(从 (K, V) 元组)、HashSet<T>、String(从 char 或 &str)、Result<Vec<T>, E>(遇第一个 Err 短路)。

iter::repeat / once / from_fn

use std::iter;

let zeros: Vec<i32> = iter::repeat(0).take(5).collect();    // [0,0,0,0,0]
let one: Vec<&str> = iter::once("hi").collect();            // ["hi"]

let mut n = 0;
let squares: Vec<i32> = iter::from_fn(|| {
    n += 1;
    if n > 5 { None } else { Some(n * n) }
}).collect();
println!("{zeros:?} {one:?} {squares:?}");

iter::repeat(x) 是无限的,配 take(n) 用。iter::once(x) 只产出一个值。iter::from_fn 用一个返回 Option<T> 的闭包构造自定义迭代器,返回 None 就结束。

collect::<Result<Vec<_>, _>>(),遇错短路

let strs = vec!["1", "2", "3"];
let nums: Result<Vec<i32>, _> = strs.iter().map(|s| s.parse::<i32>()).collect();
println!("{nums:?}");                       // Ok([1, 2, 3])

let bad = vec!["1", "x", "3"];
let r: Result<Vec<i32>, _> = bad.iter().map(|s| s.parse::<i32>()).collect();
println!("{r:?}");                          // Err(...)

源迭代器产 Result<T, E> 时,collect 到 Result<Vec<T>, E> 会在第一个 Err 处短路并返回它。给一个序列做"可能失败的操作"并暴露第一个失败的地道写法。

flat_map 与 flatten

let words = ["rust", "go"];

// flat_map:每个元素映射成一个迭代器,再摊平
let chars: Vec<char> = words.iter().flat_map(|w| w.chars()).collect();
println!("{chars:?}");          // ['r','u','s','t','g','o']

// flatten:把嵌套迭代器 / Option 摊平一层
let nested = vec![vec![1, 2], vec![3], vec![]];
let flat: Vec<i32> = nested.into_iter().flatten().collect();
println!("{flat:?}");           // [1,2,3]

flat_map(f) 把每个元素映射成一个迭代器再首尾拼接,相当于 map 加 flatten。flatten() 去掉一层嵌套(Vec<Vec<T>> 变 Vec<T>,对 Option / Result 则只保留 Some / Ok)。

filter_map

let raw = ["1", "two", "3", "x", "5"];

// 一步完成 "尝试转换 + 丢掉失败的"
let nums: Vec<i32> = raw.iter()
    .filter_map(|s| s.parse::<i32>().ok())
    .collect();
println!("{nums:?}");           // [1, 3, 5]

filter_map(f) 应用返回 Option<U> 的 f:保留 Some(u),丢弃 None。它把 filter 和 map 融成一步,是"逐个解析、跳过失败"的地道写法,比 .map(...).filter(...).map(...) 干净。

scan,带状态的 map

let nums = [1, 2, 3, 4];

// 累计前缀和
let prefix: Vec<i32> = nums.iter().scan(0, |acc, &x| {
    *acc += x;
    Some(*acc)                  // 返回 None 可提前结束
}).collect();
println!("{prefix:?}");         // [1, 3, 6, 10]

scan(初值, |state, x| ...) 像 fold,但每步产出一个元素而不是只给一个最终值,做累计和 / 前缀和正合适。闭包返回 None 可提前结束迭代。

peekable,预看下一个

let mut it = [1, 1, 2, 3, 3].iter().peekable();
let mut groups = Vec::new();

while let Some(&x) = it.next() {
    let mut count = 1;
    while it.peek() == Some(&&x) {   // 不消费,先偷看
        it.next();
        count += 1;
    }
    groups.push((x, count));
}
println!("{groups:?}");         // [(1,2),(2,1),(3,2)]

.peekable() 让你用 .peek()(返回 Option<&T>)偷看下一个元素而不消费它。需要前瞻的解析、按相同元素分组(决策取决于下一个是什么)时离不开它。

min_by_key / max_by_key

let words = ["rust", "go", "python", "c"];

let longest = words.iter().max_by_key(|w| w.len());
let shortest = words.iter().min_by_key(|w| w.len());
println!("{longest:?} {shortest:?}");   // Some("python") Some("go")

// 浮点要用 min_by + partial_cmp(无 Ord)
let floats = [3.1, 1.4, 2.7];
let min = floats.iter().copied()
    .min_by(|a, b| a.partial_cmp(b).unwrap());
println!("{min:?}");            // Some(1.4)

max_by_key / min_by_key 按提取的键比较元素,写"最长字符串""最新记录"很简洁。平局时分别返回最后 / 最前一个。浮点(无 Ord)用 max_by / min_by 配 partial_cmp。

partition,一分为二

let nums = 1..=10;

let (evens, odds): (Vec<i32>, Vec<i32>) =
    nums.partition(|n| n % 2 == 0);

println!("{evens:?}");          // [2,4,6,8,10]
println!("{odds:?}");           // [1,3,5,7,9]

partition(谓词) 消费迭代器并分成两个集合:谓词为真的一组、其余的一组。一趟遍历,两半都物化。目标类型从解构里推断。

async (10)

async fn 与 await

async fn fetch_id() -> u32 { 42 }

async fn run() -> u32 {
    let id = fetch_id().await;          // 在 await 处可能挂起
    id * 2
}

// 调用 async fn 拿到的是 Future;要 runtime 才能跑:
// #[tokio::main] async fn main() { println!("{}", run().await); }

async fn 返回 Future,必须在 runtime(tokio、async-std)里 await 才会运行。.await 在该点挂起当前任务,值没准备好时把控制权交还给 runtime。

tokio::main 与 tokio::spawn

// Cargo.toml: tokio = { version = "1", features = ["full"] }
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let h = tokio::spawn(async {
        sleep(Duration::from_millis(50)).await;
        "from task"
    });
    let val = h.await.unwrap();         // 等任务完成拿返回值
    println!("{val}");
}

#[tokio::main] 把 async main 包成同步入口,启动 runtime。tokio::spawn 在 runtime 上调度一个 async 任务,返回 JoinHandle,await 它能拿到任务返回值(或 JoinError)。

tokio::join!,并发 await

use tokio::time::{sleep, Duration};

async fn slow(n: u32) -> u32 {
    sleep(Duration::from_millis(50)).await;
    n
}

#[tokio::main]
async fn main() {
    let (a, b, c) = tokio::join!(slow(1), slow(2), slow(3));
    println!("{a} {b} {c}");            // 大约 50ms 而不是 150ms
}

tokio::join! 在同一任务里并发轮询多个 future,返回所有结果的元组。总时间等于最慢的一个,不是相加。要在 Err 处短路用 try_join!。

tokio::select!,谁先就绪取谁

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    tokio::select! {
        _ = sleep(Duration::from_millis(50)) => println!("timer fired"),
        _ = async { /* maybe blocks forever */ } => println!("other done"),
    }
}

tokio::select! 同时等多个 future,谁先就绪就跑谁的分支,其它的被 drop。常用于超时(拿 sleep 跟正事赛跑)和"多选一"。

spawn 要求 Send + sync

// ❌ Rc<T> 不是 Send,不能在 spawn 的任务里持有跨 await
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let shared = Arc::new(42);          // Arc 是 Send
    let s = shared.clone();
    tokio::spawn(async move {
        println!("{s}");
    }).await.unwrap();
}

tokio::spawn 要求 future 是 Send,因为任务可能在工作线程之间迁移。意味着任何跨 .await 持有的东西都得是 Send。共享所有权用 Arc 而不是 Rc。

.await 不会阻塞线程

// ❌ 别在 async 里这么写:阻塞整个 runtime 线程
async fn bad() {
    std::thread::sleep(std::time::Duration::from_secs(1));
}

// ✅ 用 async 等价版本
async fn good() {
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}

// 真要做 CPU 密集事,丢到 blocking 线程池:
// tokio::task::spawn_blocking(|| heavy_cpu_work()).await.unwrap();

永远不要在 async fn 里调 std::thread::sleep 或阻塞 I/O,会卡住 runtime 工作线程、阻塞所有其它任务。用 async 等价版本(tokio::time::sleep、tokio::fs ...)或把 CPU 密集任务丢到 spawn_blocking。

async 块与 Future

// async 块产生一个 Future(不是 fn 也能 async)
let fut = async {
    let a = 1;
    let b = 2;
    a + b
};

// fut 此刻还没跑,await 才执行:
// #[tokio::main] async fn main() { println!("{}", fut.await); }

// 返回 impl Future 的函数
fn make() -> impl std::future::Future<Output = i32> {
    async { 42 }
}

async { ... } 是个表达式,求值得到一个匿名 Future,跟 async fn 的函数体一样。在 runtime 里 .await 之前什么都不跑。函数可以返回 impl Future<Output = T>,把一段惰性计算交给调用方。

tokio::time::timeout

use tokio::time::{timeout, sleep, Duration};

#[tokio::main]
async fn main() {
    let slow = async { sleep(Duration::from_secs(5)).await; "done" };

    match timeout(Duration::from_millis(100), slow).await {
        Ok(v)  => println!("got {v}"),
        Err(_) => println!("timed out"),   // Elapsed error
    }
}

timeout(时长, future) 让一个 future 跟计时器赛跑,返回 Result<T, Elapsed>,future 先完成就 Ok,超时就 Err。给任意 async 操作加时间上限最干净的方式,不用手写 select!。

futures join_all,批量并发

// Cargo.toml: futures = "0.3"
use futures::future::join_all;

async fn fetch(id: u32) -> u32 { id * 2 }

#[tokio::main]
async fn main() {
    let futs = (1..=5).map(fetch);          // 5 个 Future
    let results: Vec<u32> = join_all(futs).await;
    println!("{results:?}");                // [2,4,6,8,10]
}

join_all(future 迭代器) 并发驱动数量不定的 future,按输入顺序把输出收进 Vec。数量编译期未知时用它(跟固定元数的 join! 宏不同)。

async 通道 mpsc

use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel::<i32>(8);   // 缓冲 8

    tokio::spawn(async move {
        for i in 0..3 { tx.send(i).await.unwrap() }
    });   // tx drop 后通道关闭

    while let Some(v) = rx.recv().await {
        println!("got {v}");        // 0, 1, 2
    }
}

tokio::sync::mpsc 是多生产者单消费者的 async 通道。满时 send().await 会施加背压;所有发送端 drop 后 recv().await 产出 None,所以 while let 能干净地排空。任务间传递工作的 async 方式。

智能指针 (11)

Box<T>,堆上分配

let b = Box::new(42);                   // 在堆上放一个 i32
println!("{b}");                        // 自动解引用

// 递归类型必须 Box,否则大小无限
enum Tree {
    Leaf,
    Node(i32, Box<Tree>, Box<Tree>),
}
let _t = Tree::Node(1, Box::new(Tree::Leaf), Box::new(Tree::Leaf));

Box<T> 是最简单的智能指针:一次堆分配、单一所有者、自动解引用。用来把大值放堆上、让递归类型有确定大小、存 trait object(Box<dyn Trait>)。

Rc<T>,单线程共享所有权

use std::rc::Rc;

let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a);                  // 只复制计数,不复制字符串
let c = a.clone();                      // 同上

println!("{} refs", Rc::strong_count(&a));   // 3
println!("{a} {b} {c}");

Rc<T> 跟踪引用计数;最后一个 Rc 离开作用域时值才被 drop。Rc 不是线程安全的,多线程用 Arc。需要内部可变就配 RefCell。

Arc<T>,线程间原子引用计数

use std::sync::Arc;
use std::thread;

let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for _ in 0..3 {
    let d = Arc::clone(&data);
    handles.push(thread::spawn(move || {
        println!("{:?}", d);
    }));
}
for h in handles { h.join().unwrap() }

Arc<T> 是原子引用计数,可在线程间共享。原子操作让它比 Rc 稍慢。线程间还要可变就配 Mutex / RwLock,Arc<Mutex<T>> 是地道的"共享可变"组合。

RefCell<T>,内部可变

use std::cell::RefCell;

let cell = RefCell::new(vec![1, 2, 3]);
cell.borrow_mut().push(4);              // 运行期借用检查
println!("{:?}", cell.borrow());        // [1, 2, 3, 4]

// ❌ 同时一个 borrow_mut + 一个 borrow 会运行时 panic
// let _r = cell.borrow();
// let _m = cell.borrow_mut();

RefCell 把借用检查从编译期挪到运行期,违反规则就 panic。只能单线程。用于通过共享(&)引用修改数据,常组合成 Rc<RefCell<T>>。

Mutex / RwLock,线程间加锁

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
    let c = Arc::clone(&counter);
    handles.push(thread::spawn(move || {
        let mut guard = c.lock().unwrap();
        *guard += 1;
    }));
}
for h in handles { h.join().unwrap() }
println!("{}", *counter.lock().unwrap());   // 5

Mutex<T> 串行化访问,同一时刻一个读 / 写者,.lock() 拿到守卫。RwLock 允许多读或一写。线程间共享可变状态一律用 Arc<Mutex<T>>。

Weak<T>,打破 Rc 循环引用

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    val: i32,
    parent: RefCell<Weak<Node>>,        // 父用 Weak 避免循环
    children: RefCell<Vec<Rc<Node>>>,
}

let leaf = Rc::new(Node {
    val: 3,
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![]),
});
let parent = Rc::new(Node {
    val: 5,
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&parent);

println!("leaf parent: {:?}", leaf.parent.borrow().upgrade().map(|n| n.val));

Weak<T> 是不持有所有权的 Rc 引用,不计入强引用计数。树 / 图的父指针用 Weak,避免循环引用造成内存泄漏。.upgrade() 拿回 Option<Rc<T>>。

Cow<T>,写时复制

use std::borrow::Cow;

fn normalize(s: &str) -> Cow<str> {
    if s.contains(' ') {
        Cow::Owned(s.replace(' ', "_"))     // 需要修改 → 拥有
    } else {
        Cow::Borrowed(s)                    // 没改 → 借用
    }
}

let a = normalize("no_space");          // 0 分配
let b = normalize("has space");         // 1 次分配
println!("{a} {b}");

Cow<T> = Borrowed(&T) | Owned(T)。不需要修改时零分配,真要改时再克隆并拥有。"有时候要改"的字符串处理最合适。

Rc<RefCell<T>>,共享可变

use std::rc::Rc;
use std::cell::RefCell;

let shared = Rc::new(RefCell::new(vec![1, 2]));
let alias = Rc::clone(&shared);

shared.borrow_mut().push(3);    // 通过任一别名改
alias.borrow_mut().push(4);

println!("{:?}", shared.borrow());   // [1,2,3,4]

Rc<RefCell<T>> 是单线程的"共享 + 可变"组合:Rc 给多个所有者,RefCell 给内部可变(运行期借用检查)。树 / 图常用。线程安全的对应是 Arc<Mutex<T>>。

Cell<T>,Copy 类型内部可变

use std::cell::Cell;

struct Counter { hits: Cell<u32> }

let c = Counter { hits: Cell::new(0) };
c.hits.set(c.hits.get() + 1);   // 通过 &self 也能改
c.hits.set(c.hits.get() + 1);
println!("{}", c.hits.get());   // 2

Cell<T> 用 get() / set() 给 Copy 类型提供内部可变,不借用、不做运行期检查、不会 panic,只是整值替换。T 是 Copy 且只需整值读写(不取内部引用)时,比 RefCell 更轻。

OnceCell / OnceLock,惰性初始化

use std::sync::OnceLock;

// 线程安全的"初始化一次"全局
static CONFIG: OnceLock<String> = OnceLock::new();

fn config() -> &'static str {
    CONFIG.get_or_init(|| {
        // 只在第一次调用时计算
        "loaded".to_string()
    })
}

println!("{}", config());       // loaded
println!("{}", config());       // 复用同一个值

OnceLock<T>(线程安全)和 OnceCell<T>(单线程)保存一个最多初始化一次的值。get_or_init(|| ...) 只在首次访问时跑闭包,是替代 lazy_static / once_cell 全局的稳定地道写法。

Arc<RwLock<T>>,多读单写

use std::sync::{Arc, RwLock};
use std::thread;

let cache = Arc::new(RwLock::new(vec![1, 2, 3]));

// 多个读者并发
let r = Arc::clone(&cache);
let h = thread::spawn(move || {
    let guard = r.read().unwrap();      // 共享读锁
    println!("{:?}", *guard);
});
h.join().unwrap();

cache.write().unwrap().push(4);         // 独占写锁
println!("{:?}", *cache.read().unwrap());

RwLock 允许多个并发读者或一个独占写者,读远多于写时比 Mutex 好。.read() / .write() 返回守卫。用 Arc 包起来跨线程共享:Arc<RwLock<T>>。读压力大时注意写者饥饿。

模块与 Cargo (6)

mod / pub / use,可见性

mod auth {
    pub fn login() { println!("login") }
    fn internal() {}                    // 默认 private,外部看不见
    pub mod token {
        pub fn issue() {}
    }
}

use auth::token;                        // 引到当前作用域

fn main() {
    auth::login();
    token::issue();
}

默认私有,pub 暴露。mod 声明模块(内联 {} 或文件 mod.rs / 子文件)。use 把路径引入当前作用域,免得每次都写全。

Cargo workspace 结构

// 顶层 Cargo.toml:
[workspace]
members = ["core", "cli", "server"]
resolver = "2"

// 子 crate 之间互相依赖:
// cli/Cargo.toml:
[dependencies]
core = { path = "../core" }

workspace 把多个相关 crate 组在一起,共用 Cargo.lock 和 target/,构建快得多、依赖也统一。resolver = "2" 是现代 feature 统一所必需。

pub use 重新导出

// internal::api 是实现细节,但 lib 想让用户用 mycrate::Client
mod internal {
    pub mod api {
        pub struct Client;
    }
}

pub use internal::api::Client;          // 对外重命名 / 提升路径

// 调用方:  use mycrate::Client;

pub use 把一个项目以另一个(通常更浅的)路径重新导出。常用场景:内部模块结构可以随便重排,对外保持稳定、扁平的公开 API。

use 分组导入与 as 别名

use std::collections::{HashMap, HashSet, BTreeMap};   // 一行多个
use std::io::Result as IoResult;                      // 重名时取别名
use std::fmt::Result as FmtResult;

fn _read() -> IoResult<()> { Ok(()) }
let _m: HashMap<i32, i32> = HashMap::new();
let _s: HashSet<i32> = HashSet::new();

use a::{B, C, D} 一行从同一路径导入多个项。use a::Thing as Alias 在导入时重命名,两个路径导出同名(std::io::Result 和 std::fmt::Result)时必须用。

crate / super / self 路径前缀

mod network {
    pub fn connect() {}

    pub mod client {
        pub fn run() {
            super::connect();           // 上一级模块
            crate::network::connect();  // 从 crate 根开始
            self::helper();             // 当前模块
        }
        fn helper() {}
    }
}

路径可以用 crate::(crate 根)写绝对路径,或用 self::(当前模块)、super::(父模块)写相对路径。稳定的绝对路径优先 crate::,访问同级用 super::,两者都比长链条更抗重构。

cfg 属性,条件编译

#[cfg(target_os = "linux")]
fn platform() -> &'static str { "linux" }

#[cfg(not(target_os = "linux"))]
fn platform() -> &'static str { "other" }

#[cfg(feature = "extra")]      // 仅当启用了 Cargo feature "extra"
fn extra() { println!("extra on") }

println!("{}", platform());

#[cfg(...)] 只在条件成立时才编译某项:target_os、target_arch、feature = "名"、test、debug_assertions。可配 all(...) / any(...) / not(...)。cfg!(...) 宏做同样的事,写起来像运行期 bool,但仍是编译期决定。

测试 (6)

#[test] 与 assert_eq!

pub fn add(a: i32, b: i32) -> i32 { a + b }

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

    #[test]
    fn adds() {
        assert_eq!(add(2, 3), 5);
        assert_ne!(add(2, 3), 6);
        assert!(add(2, 3) > 0, "should be positive");
    }
}
// cargo test

单元测试放在代码旁边的 #[cfg(test)] mod tests。每个 #[test] 函数独立运行。assert! / assert_eq! / assert_ne! 失败时会显示 diff 和可选的错误信息。

#[should_panic] 与返回 Result 的测试

#[test]
#[should_panic(expected = "divide by zero")]
fn panics_on_zero() {
    let _ = 1 / 0_i32;                  // 真正运行期 panic
}

#[test]
fn returns_result() -> Result<(), String> {
    let n: i32 = "42".parse().map_err(|e: std::num::ParseIntError| e.to_string())?;
    assert_eq!(n, 42);
    Ok(())
}

#[should_panic(expected = "...")] 只有测试 panic 且消息包含该字符串时才通过。测试还可以返回 Result<(), E>,这样里面能用 ?,比到处 .unwrap() 干净得多。

tests/ 目录下的集成测试

// 项目根目录:
// tests/
//   smoke.rs

// tests/smoke.rs
use mycrate::add;                       // 像外部用户一样 import

#[test]
fn smoke() {
    assert_eq!(add(1, 1), 2);
}
// cargo test --test smoke

tests/ 目录下每个文件被当成独立 crate 编译,只能调你的公开 API(正好验证对外接口)。一个文件一个测试二进制;#[cfg(test)] 是隐式的。

doctest,文档里的可运行示例

/// Adds two numbers.
///
/// # Examples
///
/// ```
/// use mycrate::add;
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
// cargo test 会编译并跑 ``` 块

文档注释里的代码块会被 cargo test 编译并运行,保证文档示例永远不过时。setup 行用前缀 # 隐藏,让展示出的示例保持干净。

#[ignore] 与 --ignored

#[test]
fn fast() { assert!(1 + 1 == 2) }

#[test]
#[ignore = "slow: hits the network"]
fn slow_integration() {
    // 默认 cargo test 跳过
    // 单独跑: cargo test -- --ignored
}
// cargo test -- --include-ignored  # 全跑

给慢或依赖环境的测试加 #[ignore](可附原因),默认 cargo test 就跳过。用 cargo test -- --ignored 单独跑被跳过的,或用 --include-ignored 全跑。

#[cfg(test)] 模块与测试辅助

pub fn slugify(s: &str) -> String {
    s.trim().to_lowercase().replace(' ', "-")
}

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

    fn check(input: &str, want: &str) {   // 测试内部辅助函数
        assert_eq!(slugify(input), want);
    }

    #[test]
    fn slugifies() {
        check("Hello World", "hello-world");
        check("  Rust  ", "rust");
    }
}

#[cfg(test)] mod tests 块只在测试时编译,里面的辅助函数和 fixture 不会给发布二进制增加任何体积。use super::* 引入父模块的项。把重复断言抽成 check() 辅助函数。

常见坑 (13)

坑:多字节字符上切 &str

let s = "中文";
// let bad = &s[0..1];     // ❌ panic: not a char boundary

// ✅ 用 .chars() 按字符切
let first: String = s.chars().take(1).collect();
println!("{first}");        // 中

// ✅ 已知字节边界时可以 &s[..n]
let s2 = "abc";
println!("{}", &s2[..1]);   // a

Rust 字符串是 UTF-8 字节,len() 是字节数。切片 &s[i..j] 按字节走,(i, j) 没落在字符边界就 panic。要按字符切用 .chars()、.char_indices() 或 split_at_checked 之类的方法。

坑:整数溢出只在 debug 模式 panic

let x: u8 = 255;
// debug: panic 'attempt to add with overflow'
// release: 静默 wrap 到 0
// let y = x + 1;

// ✅ 显式选择行为
let wrap = x.wrapping_add(1);           // 0
let sat  = x.saturating_add(1);         // 255
let chk: Option<u8> = x.checked_add(1); // None
println!("{wrap} {sat} {chk:?}");

Rust 在 debug 构建下整数溢出 panic,release 构建下静默回绕。绝对不要隐式依赖溢出行为,用 wrapping_* / saturating_* / checked_* / overflowing_* 把意图说清楚。

坑:拿 .clone() 糊借用检查器

// ❌ 闭包要 'static → 不思考直接 clone 大对象
let big = vec![0_u8; 10_000_000];
let _bad = move || println!("{}", big.clone().len());

// ✅ 真正需要的是 Arc 共享所有权
use std::sync::Arc;
let big = Arc::new(vec![0_u8; 10_000_000]);
let b = Arc::clone(&big);               // 只增加引用计数
let _good = move || println!("{}", b.len());

为了让借用检查器闭嘴随手 .clone() 经常是在掩盖真正的所有权问题。停一下问自己:"我要的是共享所有权(Arc)、内部可变(RefCell),还是单纯借用(&)?"乱 clone 会撑内存、拖性能。

坑:值被 move 后又用

let v = vec![1, 2, 3];
let w = v;
// println!("{:?}", v);     // ❌ borrow of moved value

// ✅ 想保留 v 就 clone 或借用
let v = vec![1, 2, 3];
let w = v.clone();
println!("{v:?} {w:?}");

非 Copy 类型赋值会 move 掉源值,之后再用就编译报错。两边都要就 .clone() 或者重构成传 &v。仔细读编译错误:编译器会指出 move 发生在哪一行。

坑:Vec 扩容会让指针失效

let mut v = vec![1, 2, 3];
let first = &v[0];          // 借用第一个元素
// v.push(4);               // ❌ 借用还活着 → 编译期就拦下
println!("{first}");

// 借用结束后才 push
v.push(4);
println!("{v:?}");

Vec::push 可能扩容并搬移底层缓冲,让所有已有元素指针失效。借用检查器在编译期就拦下,持有 &v[i] 期间不能调可变方法。C / C++ 里这是 use-after-free,Rust 里压根编不过。

坑:panic 后 Mutex 中毒

use std::sync::{Arc, Mutex};
use std::thread;

let m = Arc::new(Mutex::new(0));
let m2 = Arc::clone(&m);
let _ = thread::spawn(move || {
    let _guard = m2.lock().unwrap();
    panic!("oops");                     // 持锁 panic → poison
}).join();

// 主线程 lock 会拿到 Err(Poisoned)
match m.lock() {
    Ok(_)  => println!("clean"),
    Err(e) => println!("poisoned, recover: {}", *e.into_inner()),
}

持有 Mutex 时 panic 会把锁"毒化",之后的 .lock() 返回 Err(PoisonError)。多数情况可以 .into_inner() 恢复,但要有意识地决定,不要不思考就直接 .unwrap() 锁。

坑:async 里跨 await 持 std::sync::Mutex

// ❌ std::sync::Mutex 是阻塞锁,跨 .await 持有会卡线程
use std::sync::Mutex;
async fn bad(m: &Mutex<i32>) {
    let g = m.lock().unwrap();
    some_async().await;                 // 整段时间锁都没释放
    println!("{g}");
}
async fn some_async() {}

// ✅ async 场景用 tokio::sync::Mutex
use tokio::sync::Mutex as TMutex;
async fn good(m: &TMutex<i32>) {
    let g = m.lock().await;
    some_async().await;
    println!("{g}");
}

std::sync::Mutex 阻塞 OS 线程,跨 .await 持有会让 runtime 没法复用该线程,致命。要么 await 之前 drop guard,要么用 tokio::sync::Mutex / async_lock::Mutex,它们的 lock() 本身就是 async fn。

坑:闭包捕获范围超出预期

struct Big { name: String, data: Vec<u8> }
let b = Big { name: "n".into(), data: vec![0; 10_000_000] };

// ❌ 看似只用了 name,但 Rust 早期会把整个 b 借进闭包
let f = || println!("{}", b.name);
// 2021 edition 起的 "disjoint capture" 修了这个:现在只借 b.name
f();
println!("{}", b.data.len());

Rust 2021 之前,闭包即便只用到一个字段也会把整个 struct 捕获,挡住其它字段的使用。2021 edition 引入"按字段捕获":只捕获真正用到的字段。维护 2018 老代码要注意。

坑:浮点数用 == 比较

let a = 0.1 + 0.2;
println!("{a}");                // 0.30000000000000004
// if a == 0.3 { ... }          // ❌ 几乎永远不相等

// ✅ 比较绝对误差
let eq = (a - 0.3).abs() < 1e-9;
println!("{eq}");               // true

浮点运算不精确,所以 0.1 + 0.2 不等于 0.3。绝不要用 == 比浮点;改判 (a - b).abs() < epsilon,或在需要精确时(钱!)用整数 / 定点 / decimal crate。

坑:下标越界 vs .get

let v = vec![1, 2, 3];

// let x = v[10];               // ❌ 运行期 panic: index out of bounds

// ✅ .get 返回 Option,安全
match v.get(10) {
    Some(x) => println!("{x}"),
    None    => println!("out of range"),
}
let y = v.get(1).copied().unwrap_or(-1);
println!("{y}");                // 2

下标 v[i] / s[i] 越界会 panic,索引本来就该有效时没问题,索引来自外部输入时就是致命的。任何你不完全掌控的索引都用 .get(i) → Option<&T>(或 .get_mut)。

坑:变量遮蔽藏 bug

let config = load();
let config = validate(&config);     // 有意:逐步精炼同一概念

// ❌ 无意遮蔽:手滑把不同含义塞进同名变量
let count = 10;
let count = "ten";                  // 类型 / 含义都变了,后续用错很难发现
println!("{count}");

fn load() -> String { "raw".into() }
fn validate(s: &str) -> String { s.to_uppercase() }

遮蔽(用同名 let 重新绑定)在逐步精炼同一概念时是有意且地道的。但不小心遮蔽一个无关的值会悄悄改变它的类型 / 含义、藏住逻辑 bug,含义不同的东西就起不同的名字。

坑:collect 收错类型

let pairs = vec![("a", 1), ("a", 2), ("b", 3)];

// ❌ 以为是 Vec,写成 HashMap → 同 key 后者覆盖前者
use std::collections::HashMap;
let m: HashMap<&str, i32> = pairs.iter().copied().collect();
println!("{:?}", m.get("a"));   // Some(2),丢了 ("a", 1)

// ✅ 真要保留全部,用 Vec 或分组
let grouped: HashMap<&str, Vec<i32>> = pairs.iter().fold(
    HashMap::new(),
    |mut acc, &(k, v)| { acc.entry(k).or_default().push(v); acc },
);
println!("{:?}", grouped.get("a"));   // Some([1, 2])

collect() 到 HashMap 时,重复键只悄悄保留最后一个值,键重复时很容易丢数据。要保留所有值就收进 Vec,或 fold 成 HashMap<K, Vec<V>>。务必清楚目标类型的语义。

坑:迭代器惰性,什么都没做

let v = vec![1, 2, 3];

// ❌ map 是惰性的,没有消费者 → 闭包根本不执行
// v.iter().map(|x| println!("side {x}"));   // 啥也不打印(还会有 warning)

// ✅ 要驱动它:for_each / collect / sum / count ...
v.iter().for_each(|x| println!("side {x}"));

迭代器适配器(map、filter、inspect)是惰性的,只搭管道,要有消费型适配器(for_each、collect、sum、count、for 循环)才会跑。光写 .map(...) 带副作用会一次都不执行。

这个工具能做什么

可搜索的 Rust 速查表,覆盖日常真在撸的 100+ 段地道 代码,不是凑数的 hello world。十五大分类:基础 (let / mut / 变量遮蔽、整数浮点 char 类型族、 String 与 &str 区别、if 作表达式、fn、match 含守卫), 所有权(默认 move 转移、&T 共享借用、&mut T 独占借 用、Copy 与 Clone 区别、借用检查器两条铁律、用引用 避免 clone、Drop 与 RAII、mem::take),生命周期 ('a 标注、3 条省略规则、'static、持有引用的结构 体、NLL 非词法生命周期),struct 与 enum(new()、元 组 / 单元结构体、带数据的 enum、Option 替代 null、 Result、常用 derive、结构体更新语法),trait(默认 方法、孤儿规则、约束、dyn Trait 动态分发、impl Trait 静态分发、From / Into、Display 与 Debug、 newtype、关联类型 vs 泛型参数),泛型(泛型函数 / struct、where 子句、const 泛型、单态化、turbofish), 集合(Vec、iter / iter_mut / into_iter、HashMap、 HashSet、BTreeMap、String 操作、VecDeque、sort), 错误处理(? 运算符、Box<dyn Error>、自定义错误 enum、panic!、unwrap_or、main 返回 Result、? 触发 From 转换、never 类型 !),闭包(Fn / FnMut / FnOnce、move、返回闭包、配迭代器),迭代器(map / filter / collect、fold、enumerate / zip、take / skip / chain / rev、any / all / find、collect 到 HashMap / String、iter::from_fn、collect 到 Result 短路),async(async fn 与 await、tokio::main + spawn、join! 并发、select! 谁先就绪取谁、spawn 的 Send 要求、.await 不能阻塞线程),智能指针(Box、 Rc、Arc、RefCell、Mutex / RwLock、Weak 打破循环引 用、Cow),模块与 Cargo(mod / pub / use、 workspace、pub use 重新导出),测试(#[test]、 should_panic、返回 Result 的测试、tests 目录集成测 试、文档 doctest),以及 8 个真烧时间的坑(多字节 字符上切 &str 直接 panic、整数溢出只在 debug panic release 静默回绕、拿 .clone() 糊借用检查器其实在掩 盖所有权问题、值被 move 后又用、Vec 扩容让 &v[i] 失效、持锁 panic 让 Mutex 中毒、async 里跨 await 持 std::sync::Mutex 卡 runtime、闭包按字段捕获在 2021 之前会全捕获 struct)。每条都带:双语标题、 可复制的真实代码、双语说明。搜索跨标题 / 代码 / 中 英说明一起过滤,分类胶囊缩范围。中英文都是为对应语 言用户独立撰写,不是机翻。配 Go / Python / TypeScript / PostgreSQL 速查覆盖整条技术栈。

工具细节

输入
文本
页面会根据工具类型展示文本框、数值控件、文件选择或结构化输入。
输出
即时结果 + 复制
结果区优先给出可操作结果,支持项会显示复制、下载或可视化预览。
隐私
可能使用网络查询
组件源码里检测到网络调用,页面会按工具逻辑处理;敏感内容建议先脱敏。
保存 / 分享
免账号使用
打开页面即可使用;刷新后是否保留结果取决于具体工具。
性能预算
首屏 JS ≤ 30 KB
没有声明 WASM 依赖,适合快速打开和移动端使用。
适用场景
开发运维 · 程序员
分类和职业标签用于推荐相关工具、组织内链,并帮助用户快速判断是否适合当前任务。

怎么用

  1. 1. 输入

    把内容粘贴或拖入工具面板。

  2. 2. 处理

    点击按钮,在浏览器内本地处理,文件不上传。

  3. 3. 复制 / 下载

    一键复制结果或下载到本地。

Rust 速查表 适合怎么用

适合穿插在写代码、查问题、做 Review、上线前的小任务里。

适合开发场景

  • 格式化、校验、压缩或检查和代码相关的文本。
  • 把片段整理好再放进文档、工单、提交或交接材料。
  • 不切换工具,快速检查一个小 payload。

开发检查项

  • 压缩、混淆这类不可逆处理,先对副本操作。
  • 除非确认工具本地处理,不要粘贴密钥和敏感片段。
  • 转换后的代码上线前,仍要跑自己的测试或 lint。

下一步可以接着做

这些入口会把当前任务接到更完整的工具链里。

  1. 1 JSON 格式化与校验 浏览器内即时格式化、校验、压缩 JSON,数据不离开本地。 打开
  2. 2 Go (Golang) 速查表 Go (Golang) 速查表,100+ 段地道 Go 代码,语法/goroutine/channel/泛型/错误/标准库。 打开
  3. 3 Python 速查表 Python 速查表,100+ 段地道 Python 代码片段,涵盖字符串/列表/字典/文件/异步,带真实例子。 打开

真实使用场景

  • 把 2000 行 Go 服务迁到 Rust,撞上借用检查器这堵墙

    你把一个 JSON 摄取服务从 Go 搬到 Rust,编译器把 40 行在 Go 里没问题的代码全打回来了。筛到「所有权」和「生命周 期」,把 move、&T、&mut T 三种放一起对照看,大多数报错 最后都归到同一个修法:别随手 .clone(),该传 &str 就别传 String,或者干脆让结构体拥有数据。在这块花 20 分钟,能 省下一整天跟 E0502 死磕。

  • 给要发布的 crate 写第一个库级错误类型

    你的二进制里到处用 Box<dyn Error>,? 直接就过,但 crate 的 reviewer 想要调用方能 match 的命名变体。翻到「错误处 理」,照抄那个带 NotFound、Io(std::io::Error)、 Conflict(String) 的 enum,加上 thiserror 的 derive,你的 ? 链照样编过,下游代码也拿到了真正能分支恢复的错误,而 不是一坨字符串。

  • 让一份 config 在 8 个 tokio 任务间共享还不出数据竞争

    你 spawn 了 8 个后台任务,每个都要读一份偶尔被其中一个 任务改写的 config 结构体。搜「Arc」「RwLock」「async」, 速查给的是每个任务克隆一份 Arc<RwLock<Config>>,外加那个 能吃掉你一下午的坑:绝不能跨 .await 持有 std::sync 的锁。 换成 tokio::sync::RwLock,高负载下 runtime 就不会被饿死。

  • 面试前一晚临时抱佛脚补 Rust

    你只剩一个晚上,明天就是共享屏幕的 Rust 面试。在搜索框 里敲「iterator」「trait」「closure」,把 30 条带可跑代码 的条目过一遍。Fn / FnMut / FnOnce 的区别、dyn 与 impl Trait、那 8 个坑,正好是面试官爱挖的点。看能在脑子里跑 起来的代码,比晚上 11 点再翻 400 页书强得多。

常见踩坑

  • 把用了 let-else 或 {变量} 简写的片段贴到老 toolchain 上报语法错误。粘之前看条目里的版本标注(let-else 要 1.65,{变量} 要 1.58)。

  • 只搜英文词比如「lifetime」结果漏条目。搜索框跨代码和中英双语,敲「生命周期」或裸 token 'a 都能翻出同一批卡片。

  • 把 Arc<Mutex<T>> 那条当成 async 里也安全。那个例子针对 std::thread,跨 .await 必须换 tokio::sync::Mutex,否则 runtime 可能死锁。

隐私说明

整个速查是单个静态页,所有片段都在你浏览器里的内存数组里。你输入的搜索词不出标签页、不进服务器、也不写进 URL。一边输入一边看 DevTools 的 Network,零请求,公司代理后面或气隙机器上都能放心用。

常见问题

类似工具组合

做你这行的人, 还会一起用这些。

Made by Toolora · 100% client-side · Updated 2026-06-13