Every Rust binary starts at fn main. println! is a macro (note the !), not a function; from 1.58 you can interpolate any in-scope identifier directly with {name}.
let, mut, shadowing
let x = 5; // 不可变
let mut y = 5; // 可变
y += 1;
// shadowing:用 let 重新绑定,类型可变
let s = "42";
let s: i32 = s.parse().unwrap(); // s 现在是 i32
Bindings are immutable by default; add mut to mutate. Shadowing reuses the name with a brand-new binding, so the type can change — different from assignment.
integer / float / bool / char types
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 integer types are explicit about signedness and width. char is a 4-byte Unicode scalar value, NOT a byte. Use _ in literals for readability and a suffix to pin the type.
tuple, array, slice
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)
Tuples are fixed-size, heterogeneous; arrays are fixed-size, homogeneous (length is part of the type); slices &[T] are a fat pointer (data ptr + length) into an array or Vec.
String vs &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 is a heap-allocated, growable, UTF-8 owned buffer. &str is a borrowed view (slice) into UTF-8 bytes. Function parameters should usually take &str so they accept both.
if as expression
let n = 7;
let parity = if n % 2 == 0 { "even" } else { "odd" };
println!("{parity}");
// 分支类型必须一致;少写 else 编译器会按 unit 类型给你 ()
let _u = if n > 0 { /* () */ };
if is an expression — it returns a value. All branches must yield the same type. Missing else gives you the unit type ().
loop / while / for ranges
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 is an infinite loop that can break with a value. for iterates anything implementing IntoIterator — ranges 0..n and 0..=n, plus .chars(), .iter(), .iter_mut() etc.
fn — function definition
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
}
Function parameters and return type are annotated. The last expression (no trailing semicolon) is the return value; use return for early exit. Multi-value return = tuple.
match — exhaustive pattern matching
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 must be exhaustive — the compiler errors if you miss a case. Supports literals, ranges 1..=9, or-patterns 1 | 2, guards (if cond), and destructuring of enums / structs / tuples.
const is inlined at compile time wherever it is used (no single address). static has a single memory location for the whole program. static mut requires unsafe to access — usually you want OnceLock / Mutex instead.
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 is match with a single arm of interest. let else (stable 1.65) binds when the pattern matches and otherwise runs a diverging block — perfect for early returns without nesting.
labeled loops + break value
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)
Loop labels 'name let break / continue target an outer loop. break can also carry a value out of a loop expression. Combined, you break the labeled loop with a result in one statement.
type cast with 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 does cheap primitive numeric casts: float-to-int truncates toward zero, narrowing wraps (never panics), char-to-int gives the code point. For fallible conversions prefer TryFrom / try_into.
block as expression
let area = {
let w = 3;
let h = 4;
w * h // 最后一行无分号 = 块的值
};
println!("{area}"); // 12
// 用块把临时变量限制在小作用域里
let cleaned = {
let raw = " hi ";
raw.trim().to_uppercase()
};
println!("{cleaned}");
A { ... } block is an expression that evaluates to its final expression (no trailing semicolon). Use it to scope temporaries tightly and compute a value inline without a helper function.
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 loops as long as the pattern keeps matching — ideal for draining a stack / queue via .pop() (which returns Option) or consuming an iterator until it yields None.
Ownership(12)
move semantics by default
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}"); // ✅ 都还能用
For non-Copy types like String, assignment / passing to a function MOVES ownership — the source binding becomes unusable. Copy types (integers, bool, f64, &T, ...) copy bit-for-bit and keep both usable.
shared borrow &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 is a shared (read-only) borrow. Many shared borrows can coexist. They do not move ownership, so the original binding stays usable after the call.
exclusive borrow &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 is an exclusive (write) borrow. At any moment there can be exactly one — and you cannot mix it with any & borrow at the same time. This rule eliminates data races at compile time.
Copy vs 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 is implicit, bit-for-bit, and only legal for types whose every field is also Copy. Clone is explicit (.clone()) and can do arbitrary work (e.g. allocate). Heap-owning types like String / Vec are Clone but NOT Copy.
borrow checker rules in one place
// 两条铁律:
// 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:?}");
Two rules: (1) at any time you either have any number of &T or exactly one &mut T, never both; (2) any reference must not outlive its referent. Non-lexical lifetimes (NLL) shrink borrows to last-use, so most "fight the borrow checker" code just rearranges to flow naturally.
When a function only needs to read, take &str / &[T] / &T — not String / Vec / T. This avoids the allocation a .clone() would cost and accepts both owned and borrowed callers.
Drop trait — RAII cleanup
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
}
When a value goes out of scope, Rust calls Drop::drop on it. This is how files close, locks release, and memory frees — without any GC. Drop order inside a scope is 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 pulls a value out of a &mut reference and leaves Default::default() behind. mem::replace lets you swap in any value. Both let you move-out of borrowed structures without violating the borrow rules.
partial move out of a struct
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
You can move individual non-Copy fields out of a struct; afterwards that field is uninitialized but the OTHER fields stay usable. The struct as a whole, however, can no longer be moved or borrowed wholesale.
reborrow &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
Passing a &mut T to a function that takes &mut T does an implicit reborrow (&mut *r), not a move — so the original &mut binding remains usable afterwards. This is why you can call bump(r) twice in a row.
Calling .into_iter() on an owned Vec yields owned T and consumes the Vec — letting you move each element out without cloning. Use this when you transform a collection and no longer need the original.
Derive Default to get a zero/empty value via Opts::default(). Combine with struct update syntax ..Default::default() to set only the fields you care about — a clean builder-lite pattern for config structs.
Lifetimes(9)
lifetime annotation 'a basics
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}");
Lifetime annotations describe the relationship between input and output references; they do not change how long anything actually lives. 'a here says: the returned reference is valid as long as BOTH inputs are.
The compiler infers lifetimes via three elision rules: each input ref gets its own lifetime; if there is one input ref, the output gets that lifetime; if &self / &mut self is present, output gets self lifetime. You only annotate when these rules cannot decide.
'static means the reference is valid for the entire program. String literals are &'static str. APIs like thread::spawn need 'static so the spawned thread cannot outlive the data it captures.
struct holding a reference
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 活得久
A struct that borrows must declare a lifetime parameter so the compiler knows it cannot outlive the data it points into. Often the right choice instead is to OWN the data (String, Vec<T>) to avoid lifetime gymnastics.
multiple lifetimes
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}");
When two input lifetimes are independent and only one matters for the return, give them different lifetime parameters. This says the result is tied to owner, not to sep — so sep can drop earlier.
NLL — non-lexical lifetimes
let mut v = vec![1, 2, 3];
let first = &v[0]; // 借用开始
println!("{first}"); // 借用最后一次使用 → 借用结束
v.push(4); // ✅ NLL 之后这条不再报错
println!("{v:?}");
Since Rust 2018, borrows end at their last use rather than at the end of the lexical scope. This lets a lot of intuitive code compile without intermediate scopes / blocks.
lifetime bound on generic 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 means every reference inside type T outlives 'a. You need this bound when a struct stores &'a T so the compiler can guarantee the borrowed data does not dangle. Often inferred, but explicit in older or complex code.
higher-ranked trait bound 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> is a higher-ranked trait bound (HRTB): the bound must hold for EVERY possible lifetime 'a, not one specific one. It shows up most when a closure / fn parameter takes a reference of any lifetime.
returning a reference tied to &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 活得久)
When a method takes &self and returns a reference, elision ties the output lifetime to self. The returned &str borrows from the struct, so the struct must outlive the returned reference — no annotation needed.
Struct & enum(13)
struct definition + 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:?}");
Convention: a constructor is a static method named new. Using impl Into<String> for the name lets callers pass &str, String, or anything convertible. Self refers to the enclosing type.
tuple struct + unit struct
// 元组结构体:字段没名字,靠位置
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;
Tuple structs name a type but not its fields — handy as newtypes. Unit structs have no fields at all, useful as trait markers or zero-sized sentinels.
enum with data
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 enums are sum types: each variant can carry different shaped data (tuple, named struct, or nothing). They model exclusive states (this OR that) and force exhaustive handling via match.
Option<T> — no nulls in Rust
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 replaces null. The compiler forces you to handle None — no NullPointerException possible. Combinators like map / and_then / unwrap_or chain transformations cleanly.
Result<T, E> = Ok(T) | Err(E) is how Rust returns recoverable errors. The compiler insists you handle (or propagate with ?) the Err arm; nothing silently throws.
#[derive(...)] common traits
#[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 auto-implements common traits if all fields support them. Debug for {:?} printing, Clone for .clone(), PartialEq/Eq for ==, Hash to use as HashMap key, Default for ::default().
struct update syntax
#[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 copies remaining fields from another value. Note: if any copied field is non-Copy (like String), this MOVES base — so base is no longer fully usable afterwards.
Inside impl, fn without self is an associated function (called as Type::name); &self / &mut self / self are method receivers. Methods that take self consume the value — they cannot be called again.
enum discriminant + as cast
#[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
A fieldless (C-like) enum can assign explicit integer discriminants and be cast to an integer with as. Handy for serializing to status codes. Note: only fieldless enums support the as cast.
matches! macro
#[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!(expr, pattern) returns a bool — true if expr matches the pattern (with optional guard). Cleaner than a full match when you only need a yes/no test against one shape.
Option combinators — 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));
Chain Option without unwrapping: map transforms the inner value, and_then (flatMap) chains another Option-returning step, filter drops Some that fails a predicate, ok_or turns None into Err. Avoids nested match pyramids.
map transforms Ok, map_err transforms Err, and_then chains a fallible step, .ok() discards the error into Option, .unwrap_or_else handles Err lazily. These build readable pipelines without nested match.
impl Iterator for custom type
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]
Implement Iterator by defining type Item and next(&mut self) -> Option<Item>. You then get the full combinator suite (map/filter/take/...) for free. Return None to end; here it never ends, so pair with take(n).
A trait is a contract of methods. Provide a default body and implementors can opt-out by overriding. Required methods (no body) must be supplied by every impl.
impl Trait for Type — orphan rule
// 想给 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
Orphan rule: you can implement Trait for Type only if Trait OR Type is defined in your crate. This prevents two crates from independently providing conflicting impls for the same (Trait, Type) pair.
trait bound on a generic fn
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 bounds restrict what types T can be. T: Display + Debug requires both. The where clause is just nicer syntax when bounds get long — same meaning as T: Bound on the angle brackets.
dyn Trait — dynamic dispatch
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() }
A Box<dyn Trait> stores a value of unknown size and dispatches the method through a vtable at runtime. Use it when you need a heterogeneous collection or to hide a concrete type behind an interface.
impl Trait return — static dispatch
// 调用方拿到一个实现了 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 in return position means "some concrete type that implements Trait; I am not telling you which". The compiler picks one type and inlines / monomorphizes — no vtable. Cannot be used to return different concrete types from different branches; use Box<dyn> for that.
From / Into — value conversion
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);
Implement From<X> for Y; you automatically get Into<Y> for X. Prefer From in your own code so callers can write x.into() or call Y::from(x). impl Into<T> on function parameters lets the function take anything convertible.
Display vs 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 ({}) is the user-facing representation; you write it by hand. Debug ({:?}) is the developer-facing dump; usually #[derive(Debug)] is enough. {:#?} pretty-prints.
Derive PartialOrd + Ord for total ordering; fields compare lexicographically. f32 / f64 only implement PartialOrd (NaN!) — use sort_by with partial_cmp().unwrap() when you know no NaN exists.
extending external types — newtype pattern
// 想给 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 = tuple struct wrapping an existing type. Lets you add methods, implement foreign traits without breaking the orphan rule, and create a distinct type so you cannot mix up UserId(u64) with PostId(u64).
Associated type = one impl per type; cleaner signatures (Iterator::Item). Generic param on a trait = one type can implement it many times for different T (From<i32>, From<u64>). Pick associated type unless you really want multiple impls.
TryFrom / TryInto — fallible conversion
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> is the fallible cousin of From<X>: try_from returns Result<Self, Error>. Implementing it also gives you TryInto for free, and is the idiomatic way to validate-on-convert (e.g. i32 → bounded Age).
Deref — smart pointer transparency
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
Implementing Deref lets you call the inner type’s methods directly and triggers deref coercion (&MyBox<String> → &str). This is how Box / Rc / String feel transparent. Do NOT abuse it to fake inheritance.
Operators are traits in std::ops: Add (+), Sub (-), Mul (*), Index ([]), etc. Implement Add with an Output associated type to make + work on your type. Other arithmetic operators follow the same shape.
A blanket impl like impl<T: Bound> Trait for T implements your trait for every type that satisfies the bound at once. This is how ToString is implemented for all Display types in std. Powerful but can cause coherence conflicts.
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 declares Display as a supertrait — every Named implementor must also implement Display, and Named’s default methods may rely on Display. Use it to require capabilities a trait depends on.
Generics(9)
generic function
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
Generic function parameters use angle brackets <T>. Trait bounds (T: PartialOrd) restrict what operations the body can perform on T. Monomorphization generates a specialized version per concrete T at compile time — zero runtime cost.
Struct generics live on the type and on each impl block. You can write an impl for a fully concrete combination (Pair<i32, i32>) to add methods only available for that specialization.
trait bound with +
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));
Combine bounds with +. Use a where clause when there are multiple type params or several bounds per param — same meaning, much more readable.
const generics let array sizes and other compile-time constants be type parameters. Lets one function work for any [T; N] without runtime allocation. Stable since 1.51 for primitive const params.
monomorphization — zero-cost generics
fn id<T>(x: T) -> T { x }
let a = id(1_i32);
let b = id("hi");
// 编译后等价于两个独立函数:id_i32 / id_str
// 调用方完全没有运行期开销
Each generic instantiation gets its own compiled function (monomorphization). No vtable, no boxing, identical perf to a hand-written specialized function — at the cost of bigger binary size.
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:?}");
When the compiler cannot infer the generic type (.collect() being the classic case), you can specify it explicitly with the turbofish ::<T>. Reads weird at first, but it is the canonical syntax.
impl Trait in argument position
// 等价于 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 in argument position is shorthand for an anonymous generic parameter: fn f(x: impl Trait) means fn f<T: Trait>(x: T). Cleaner for simple single-use bounds; use explicit <T> when you need to name the type elsewhere.
PhantomData<T> is a zero-sized marker that lets a type be generic over T without storing a T. Used for type-level tags (units, states) so the compiler keeps Length<Meters> and Length<Feet> distinct at zero runtime cost.
Generic parameters can have defaults: trait Add<Rhs = Self>. So Millis + Millis uses the default Self, but you can also impl Add<u64> to allow Millis + u64. Defaults let one trait serve both the common and custom case.
Collections(12)
Vec<T> basics
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> is the workhorse heap-allocated growable array. .push / .pop / .len / .iter cover most needs. Direct indexing v[i] panics on out-of-bounds; .get(i) returns Option<&T> for safe access.
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() yields &T (collection still usable), .iter_mut() yields &mut T, .into_iter() yields T and consumes the collection. for x in &v desugars to .iter(), for x in v to .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 is the standard hash table. .entry(k).or_insert(v) is the idiomatic "insert if absent then return &mut V" — perfect for counters. Iteration order is unspecified (and randomized per process by default for HashDoS resistance).
HashSet — uniqueness
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> is a hash table without values — used for uniqueness and set ops (union, intersection, difference). Building one from an iterator with .collect::<HashSet<_>>() dedups in O(n).
BTreeMap — sorted by key
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 keeps keys in sorted order — slightly slower than HashMap but gives ordered iteration and range queries. Reach for it when you need consistent order or ranged lookups.
String operations
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() returns BYTES not characters — use .chars().count() for character count. Common ops: .push / .push_str / .split / .replace / .to_uppercase / .trim / .starts_with. Slicing s[0..3] is OK only on a char boundary, otherwise panics.
VecDeque is an O(1) push/pop at both ends ring buffer. Use it for FIFO queues (BFS, work queues) or sliding windows where Vec::remove(0) would be O(n).
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() is stable O(n log n); .sort_unstable() is faster but does not preserve relative order of equal elements. Use .sort_by(|a, b| ...) for custom comparison and .sort_by_key(|x| ...) for sort-by-extracted-key.
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| ...) keeps elements matching a predicate in place (O(n), no new alloc). dedup() removes consecutive duplicates (sort first for full dedup). drain(range) removes and yields a sub-range, leaving the rest shifted.
HashMap entry API — 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) returns &mut V, inserting v if absent — ideal for counters. Use or_insert_with(|| expensive()) when the default is costly to build (e.g. Vec::new), so it is only created on a miss.
slice patterns + 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]
Slice patterns [first, .., last] destructure arrays/slices with a rest .. binding. windows(n) yields overlapping sub-slices of length n (great for diffs/pairs); chunks(n) yields non-overlapping blocks (last may be shorter).
BinaryHeap — priority queue
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 is a max-heap: push is O(log n), pop returns the largest in O(log n). Wrap items in std::cmp::Reverse to get a min-heap. Use it for Dijkstra, top-k, and scheduling.
Errors(12)
? operator — early return on 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}"),
}
The ? operator unwraps Ok or returns Err to the caller. The error type must be convertible (via From) to the function's declared error. Replaces 8 lines of match boilerplate with 1 character.
Box<dyn Error> — quick error type
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> is the lazy "any error" type — perfect for small binaries and prototypes. It uses dynamic dispatch and erases the concrete type. For libraries, prefer a named error enum or anyhow::Error / eyre::Report.
For libraries, define a named enum so callers can match on variants. Implement Display + Error for nice printing, and From<OtherError> so ? can auto-convert. thiserror crate generates all this with a derive.
panic! — unrecoverable
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! aborts the current thread (or whole program with abort strategy) and prints a backtrace if RUST_BACKTRACE=1. Use it for true bugs and broken invariants — not for expected error conditions; those return Result.
unwrap_or / unwrap_or_else / unwrap_or_default
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): on None/Err, use x. unwrap_or_else(|e| ...): same, but compute lazily and inspect the error. unwrap_or_default(): use Default::default() of the inner type. Pick these over .unwrap() when a fallback is reasonable.
? in main returning 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(())
}
Rust's main may return Result<(), E>. Then you can use ? right inside main; on Err the program exits with code 1 and prints the error via Debug. Great for CLI tools.
When ? sees an error of type E1 but the function returns Result<_, E2>, it calls E1.into() — which requires impl From<E1> for E2. This is how layered error enums propagate IO / parse / network errors uniformly.
! is the "never" type — for functions that never return (loop forever, panic!, exit). Because ! coerces to any type, you can use die() / panic!() in a match arm without messing up branch types.
ok_or(err) / ok_or_else(|| err) convert Option<T> into Result<T, E> — Some(v) → Ok(v), None → Err(err). Use the _else form when building the error is non-trivial so it only runs on the None path. Lets ? propagate a missing value.
error context with .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 wraps a low-level error with context (which file, which step) before propagating. In real code anyhow’s .context("...") or thiserror’s #[from] do this more ergonomically, but map_err is the std-only building block.
if let Err — handle just the error
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}"); // 失败只记一笔,不中断主流程
}
When you only care about the failure case (e.g. best-effort logging), if let Err(e) = ... handles it without forcing a full match or stopping the program. Common for fire-and-forget side effects.
assert! always runs and panics on a false condition — use it to enforce real invariants and document preconditions. debug_assert! compiles to nothing in release builds, so put expensive checks there. Both accept a custom message.
Closures(8)
closure basics
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"));
Closures use |args| body syntax. Argument and return types are usually inferred (just one call site). Multi-line bodies need braces. Closures can capture variables from the enclosing scope.
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}");
Three closure traits: Fn (call many times, read-only capture), FnMut (call many times, mutates captures), FnOnce (consumes captures so callable once). Fn is the strictest; every Fn is also FnMut and FnOnce.
move keyword — take ownership of captures
use std::thread;
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("{:?}", data); // data 被 move 进闭包
});
// println!("{:?}", data); // ❌ 已被 move
handle.join().unwrap();
Without move, a closure borrows captures. With move, captures are moved INTO the closure. Required for spawning threads or returning closures from functions — anywhere the closure outlives the enclosing scope.
Each closure has its own unsized anonymous type, so to return one you either box it (Box<dyn Fn(...)>) for dynamic dispatch or use impl Fn(...) for a single anonymous concrete type. Different branches returning different closures forces the Box version.
closure with iterator combinators
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)
Closures shine inside iterator chains: .filter / .map / .fold / .find / .any / .all. The compiler inlines them aggressively, so a chain often produces tight code identical to a hand-rolled loop.
closure capturing by reference 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
A closure captures by the least-restrictive mode it needs: by &, then &mut, then by value. move forces by-value capture regardless — required when the closure must outlive the current scope (threads, returned closures).
A plain function (and a non-capturing closure) coerces to the fn pointer type fn(A) -> B. Capturing closures cannot — they need Fn/FnMut/FnOnce. Where a function-like value is needed, fn pointers are the smallest, copyable option.
Store a closure in a struct via a generic F: Fn() field (monomorphized, no boxing) or a Box<dyn Fn()> field (one type, dynamic dispatch). Call it with parentheses: (self.field)(). Generic is faster; boxed is simpler when types vary.
Iterators(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:?}");
Iterators are lazy: map/filter just build a pipeline; .collect() (or .for_each / .sum / .count) finally drives it. Turbofish on collect — .collect::<Vec<_>>() — when the target type is ambiguous.
fold — left-to-right reduce
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(init, |acc, x| ...) is the general reduce: starts with init and threads acc through every element. .sum() / .product() / .min() are specialized folds. .reduce(|a, b|...) starts from the first element (returns 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() yields (index, item). zip(other) yields pairs until the shorter iterator runs out. Combine for indexed multi-iter loops without ever indexing manually.
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) keeps first n. skip(n) drops first n. chain(other) glues two iterators end-to-end. rev() reverses a DoubleEndedIterator. None of them allocate — combine freely.
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 short-circuit — they stop the iterator as soon as the answer is known. Cheap to use on infinite or huge iterators when you just want the first match.
collect into 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() can target any type implementing FromIterator: Vec<T>, HashMap<K, V> from (K, V) tuples, HashSet<T>, String from char or &str, Result<Vec<T>, E> to short-circuit on first error.
iter::repeat / iter::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) is infinite — pair with take(n). iter::once(x) yields exactly one value. iter::from_fn lets you build a custom iterator from a closure that returns Option<T>; None ends it.
collect::<Result<Vec<_>, _>>() — short-circuit
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(...)
When the source iterator yields Result<T, E>, collecting into Result<Vec<T>, E> short-circuits on the first Err and returns it. Idiomatic way to apply a fallible operation to a sequence and surface the first failure.
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) maps each item to an iterator and concatenates them — map + flatten in one step. flatten() removes one layer of nesting (Vec<Vec<T>> → Vec<T>, or filters Option/Result by keeping only 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) applies f returning Option<U>: keeps Some(u), drops None. It fuses filter + map and is the idiomatic way to "parse each, skip failures". Cleaner than .map(...).filter(...).map(...).
scan(init, |state, x| ...) is like fold but yields an item per step instead of one final value — perfect for running totals / prefix sums. Returning None from the closure ends the iterator early.
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() lets you look at the next item with .peek() (returns Option<&T>) without consuming it. Essential for lookahead parsing and run-length grouping where the decision depends on what comes next.
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 compare items by an extracted key — concise for "longest string", "newest record", etc. On ties they return the last / first respectively. For floats (no Ord) use max_by / min_by with partial_cmp.
partition — split into two
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(pred) consumes an iterator and splits it into two collections: items where the predicate is true, and the rest. One pass, both halves materialized. Target types are inferred from the destructuring.
async fn returns a Future — it does not run until awaited inside a runtime (tokio, async-std). .await suspends the current task at this point and yields control to the runtime if the value is not ready.
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] turns an async main into a sync entry point that boots the runtime. tokio::spawn schedules an async task on the runtime and returns a JoinHandle — awaiting it gives you the task's output (or a JoinError).
tokio::join! — concurrent 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! polls multiple futures concurrently on the same task and returns a tuple of all results. Total time = max of inputs, not sum. For results of different fallible types use try_join! to short-circuit on Err.
tokio::select! waits on multiple futures and runs the branch of whichever becomes ready first; the others are dropped. Used for timeouts (race a sleep) and "first-of-many" patterns.
Send + sync requirement on spawn
// ❌ 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 requires the future to be Send so it can migrate between worker threads. That means anything held across an .await must be Send. Use Arc instead of Rc for shared ownership.
Never call std::thread::sleep / blocking I/O inside an async fn — it stalls the runtime worker thread and blocks every other task. Use the async equivalent (tokio::time::sleep, tokio::fs, ...) or spawn_blocking for CPU-bound work.
async block + 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 { ... } is an expression that evaluates to an anonymous Future, just like an async fn body. Nothing runs until you .await it (inside a runtime). Functions can return impl Future<Output = T> to hand a lazy computation to the caller.
timeout(dur, fut) races a future against a timer and returns Result<T, Elapsed> — Ok if the future finished first, Err on timeout. The cleanest way to bound any async operation without hand-rolling select!.
futures join_all — many concurrent
// 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(iter_of_futures) drives a dynamic number of futures concurrently and collects their outputs into a Vec, in input order. Use it when the count is not known at compile time (unlike the fixed-arity join! macro).
async + channel (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 is a multi-producer single-consumer async channel. send().await applies backpressure when full; recv().await yields None once all senders drop, so a while let drains it cleanly. The async way to pass work between tasks.
Smart pointers(11)
Box<T> — heap allocation
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> is the simplest smart pointer: one heap allocation, single owner, automatic deref. Use it to put large values on the heap, to make recursive types Sized, and to store trait objects (Box<dyn Trait>).
Rc<T> — shared single-thread ownership
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> tracks a reference count; the value drops when the last Rc goes. Rc is NOT thread-safe — for multi-thread use Arc. Combine with RefCell when you also need interior mutability.
Arc<T> — atomic refcount for threads
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> = atomically reference-counted; safe to share across threads. Slightly slower than Rc due to atomic ops. For mutation across threads pair with Mutex / RwLock — Arc<Mutex<T>> is the canonical "shared mutable" combo.
RefCell<T> — interior mutability
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 moves borrow checking from compile time to runtime — panicking if rules are violated. Single-thread only. Used to mutate data through a shared (&) reference, often as Rc<RefCell<T>>.
Mutex / RwLock — locking across threads
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> serializes access — one writer / reader at a time; .lock() returns a guard. RwLock allows many readers OR one writer. Always Arc<Mutex<T>> for shared mutable state across threads.
Weak<T> = a non-owning Rc reference; does not contribute to strong count. Use it for parent pointers in trees / graphs to avoid leaks from reference cycles. Upgrade with .upgrade() → Option<Rc<T>>.
Cow<T> — clone on write
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). It lets you AVOID allocation when no modification is needed, but lazily clone-and-own when it IS. Perfect for "sometimes-mutate" string processing.
Rc<RefCell<T>> — shared mutable graph
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>> is the single-thread "shared + mutable" combo: Rc gives multiple owners, RefCell gives interior mutability with runtime borrow checks. Common for trees/graphs. The thread-safe analog is Arc<Mutex<T>>.
Cell<T> — Copy interior mutability
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> gives interior mutability for Copy types via get()/set() — no borrow, no runtime check, no panic, just a value swap. Lighter than RefCell when T is Copy and you only need whole-value get/set, not references into it.
OnceLock<T> (thread-safe) and OnceCell<T> (single-thread) hold a value initialized at most once. get_or_init(|| ...) runs the closure only on first access — the idiomatic stable replacement for lazy_static / once_cell globals.
Arc<RwLock<T>> — many readers
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 allows many concurrent readers OR one exclusive writer — better than Mutex when reads vastly outnumber writes. .read() / .write() return guards. Wrap in Arc to share across threads: Arc<RwLock<T>>. Beware writer starvation under heavy reads.
Items are private by default; pub exposes them. mod declares a module (inline {} or file mod.rs / submod.rs). use brings a path into scope so you do not have to spell it out every time.
A workspace groups multiple related crates with one Cargo.lock and one target/ dir — much faster builds and shared deps. resolver = "2" is required for the modern feature unification.
re-export with 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 re-exports an item under a different (usually shallower) path. The classic use is keeping internal module structure flexible while exposing a stable, flat public API at the crate root.
use grouping + as alias
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} imports several items from one path in a single line. use a::Thing as Alias renames on import — essential when two paths export the same name (std::io::Result vs std::fmt::Result).
Paths can be absolute from crate:: (the crate root) or relative with self:: (current module) and super:: (parent module). Prefer crate:: for stable absolute paths and super:: for sibling access — both survive refactors better than long chains.
#[cfg(...)] compiles an item only when a condition holds: target_os, target_arch, feature = "name", test, debug_assertions. Combine with all(...) / any(...) / not(...). The cfg!(...) macro does the same as a runtime-looking bool that is still compile-time.
Testing(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
Unit tests live in #[cfg(test)] mod tests next to the code. Each #[test] fn runs in isolation. assert!, assert_eq!, assert_ne! show diffs and an optional message on failure.
#[should_panic(expected = "...")] passes only if the test panics with a message containing the string. Tests can also return Result<(), E> so you can use ? inside — cleaner than .unwrap() everywhere.
Files inside the top-level tests/ directory are compiled as separate crates — they can only call your public API (good for verifying the surface). One file = one test binary; #[cfg(test)] is implicit.
doctests — examples that run
/// 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 会编译并跑 ``` 块
Code blocks in doc comments are compiled and run as tests by cargo test. Ensures examples in your documentation never go stale. Hide setup lines with a leading # to keep the displayed example clean.
#[ignore] + cargo test -- --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 # 全跑
Mark a slow or environment-dependent test with #[ignore] (optionally with a reason) so the default cargo test skips it. Run only the skipped ones with cargo test -- --ignored, or everything with --include-ignored.
The #[cfg(test)] mod tests block only compiles during testing, so helper functions and fixtures inside it add zero weight to the release binary. use super::* pulls in the parent module’s items. Factor repeated assertions into a check() helper.
Common pitfalls(13)
pitfall — &str slicing on multi-byte boundary
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
Strings in Rust are UTF-8 bytes; len() returns bytes. Slicing &s[i..j] is byte-based and panics if (i, j) does not fall on char boundaries. Use .chars(), .char_indices(), or methods like split_at_checked.
pitfall — integer overflow only panics in debug
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 panics on integer overflow in debug builds but silently wraps in release builds. Never rely on overflow behavior implicitly — use wrapping_*, saturating_*, checked_*, or overflowing_* to state your intent.
pitfall — .clone() to "fix" the borrow checker
// ❌ 闭包要 '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());
Reaching for .clone() to silence the borrow checker often hides a real ownership question. Stop, ask: "do I want shared ownership (Arc), interior mutability (RefCell), or to just borrow (&)?". Random clones bloat memory and runtime.
pitfall — moved value used again
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:?}");
For non-Copy types, assignment moves the value — using the source binding afterwards is a compile error. To keep both, either .clone() or refactor to pass &v. Read the error: the compiler points to where the value was moved.
pitfall — Vec re-alloc invalidates pointers
let mut v = vec![1, 2, 3];
let first = &v[0]; // 借用第一个元素
// v.push(4); // ❌ 借用还活着 → 编译期就拦下
println!("{first}");
// 借用结束后才 push
v.push(4);
println!("{v:?}");
Vec::push may reallocate and move the buffer, which would invalidate every existing element pointer. The borrow checker enforces this at compile time — you cannot hold a &v[i] across a mutating call. In C/C++ this is a use-after-free bug; in Rust it does not compile.
pitfall — Mutex poisoning after panic
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()),
}
A thread that panics while holding a Mutex POISONS it — subsequent .lock() returns Err(PoisonError). Often safe to recover via .into_inner(), but you must consciously decide; do not just .unwrap() the lock without thinking about what panicked.
pitfall — async + std::sync::Mutex held across await
// ❌ 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 blocks the OS thread — fatal if held across an .await because the runtime cannot reuse that thread. Either drop the guard before await, or use tokio::sync::Mutex / async_lock::Mutex whose lock() is itself an async fn.
pitfall — closure captures more than you expect
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());
Before Rust 2021, a closure captured an entire struct even if it only referenced one field — blocking other uses of the struct. 2021 edition introduced disjoint captures: only the fields actually used are captured. If you maintain 2018 code, watch out.
pitfall — float == comparison
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
Floating point arithmetic is inexact, so 0.1 + 0.2 != 0.3. Never compare floats with ==; instead check (a - b).abs() < epsilon, or use integers / fixed-point / a decimal crate when exactness matters (money!).
pitfall — index vs .get out of bounds
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
Indexing v[i] / s[i] panics on out-of-bounds — fine when an invalid index is a real bug, fatal when the index comes from input. Use .get(i) → Option<&T> (or .get_mut) for any index you do not fully control.
Shadowing (re-let with the same name) is intentional and idiomatic for progressive refinement of one concept. But accidentally shadowing an unrelated value silently changes its type/meaning and hides logic bugs — name distinct things distinctly.
collect() into a HashMap silently keeps only the last value per duplicate key — easy to lose data when keys repeat. If you need every value, collect into a Vec or fold into a HashMap<K, Vec<V>>. Always know your target type’s semantics.
Iterator adapters (map, filter, inspect) are lazy — they build a pipeline but do nothing until a consuming adapter (for_each, collect, sum, count, for-loop) drives it. A bare .map(...) with side effects silently runs zero times.
What this tool does
Searchable Rust cheat sheet, 100+ idiomatic snippets
working Rust devs actually type — not toy filler.
Fifteen sections: basics (let / mut / shadowing,
String vs &str, if as expression, fn, match with
guards), ownership (move, &T and &mut T, Copy vs
Clone, the two borrow checker rules, Drop, mem::take),
lifetimes ('a, elision rules, 'static, structs
holding refs, NLL), struct and enum (new(), enum
with data, Option, Result, derives, struct update),
traits (orphan rule, bounds, dyn Trait dynamic
dispatch, impl Trait static dispatch, From / Into,
Display vs Debug, newtype, associated type vs
generic param), generics (fn / struct, where, const
generics, monomorphization, turbofish), collections
(Vec, iter / iter_mut / into_iter, HashMap, HashSet,
BTreeMap, String, VecDeque, sort), errors (?
operator, Box<dyn Error>, custom error enum, panic!,
unwrap_or, main returning Result, From in ?, never
type !), closures (Fn / FnMut / FnOnce, move,
returning closures, iterator combos), iterators (map
/ filter / collect, fold, enumerate / zip, take /
skip / chain / rev, any / all / find, collect to
HashMap / String / Result), async (async fn,
tokio::main + spawn, join!, select!, Send on spawn,
never block in async), smart pointers (Box, Rc, Arc,
RefCell, Mutex / RwLock, Weak, Cow), modules / Cargo
(mod / pub / use, workspace, pub use), testing
(#[test], should_panic, Result-returning tests,
integration tests, doctests), and 8 real pitfalls
(UTF-8 byte slicing panics, integer overflow only
panics in debug, .clone() to silence the borrow
checker, moved value used again, Vec re-alloc
invalidating &v[i], Mutex poisoning after panic, std
Mutex held across await, closure capturing more than
expected). Every entry: bilingual title + runnable
code + bilingual description. Search across title /
code / both languages; filter by section. Native EN
/ ZH copy, written independently, not translated.
Tool details
Input
Text
The page exposes text boxes, numeric controls, file pickers, or structured inputs depending on the tool.
Output
Live result + Copy
The result area focuses on usable output, with copy, download, or preview actions when supported.
Privacy
May use a live lookup
A network call is detected in the component, so redact sensitive data when appropriate.
Save / share
No account required
Open the page and use it; whether results survive refresh depends on the tool.
Performance budget
Initial JS <= 30 KB
No WASM budget is declared, keeping the tool quick to open on mobile.
Best fit
Developer & DevOps · Developer
Category and role tags drive related tools, internal links, and quick fit checks.
How to use
1
1. Input
Paste or drop your content into the tool panel.
2
2. Process
Click the button. All processing is local in your browser.
3
3. Copy / Download
Copy the result or download to disk in one click.
How Rust Cheatsheet fits into your work
Use it in the small gaps between coding, reviewing, debugging, and shipping.
Developer jobs
Formatting, validating, shrinking, or inspecting code-adjacent text.
Preparing snippets for documentation, tickets, commits, or handoff.
Checking a small payload quickly without switching tools.
Developer checks
Run irreversible transforms like minify or obfuscate on a copy.
Keep secrets out of pasted snippets unless the tool explicitly stays local.
Use your normal tests or linter before shipping transformed code.
Good next steps
These links move the current task into a more complete workflow.
Porting a 2000-line Go service to Rust and hitting the borrow checker wall
You moved a JSON ingest service from Go to Rust and the
compiler rejects 40 lines that looked fine in Go. Filter
to "Ownership" and "Lifetimes", read move vs &T vs &mut T
side by side, and most errors collapse into one fix: stop
reaching for .clone(), pass &str instead of String, or own
the data in the struct. Twenty minutes here saves a day of
fighting E0502.
Writing your first library error type for a crate you publish
Your binary used Box<dyn Error> everywhere and ? just
worked, but reviewers on your crate want named variants
callers can match. Pull up "Errors", copy the enum with
NotFound / Io(std::io::Error) / Conflict(String), add the
thiserror derive, and your ? chains keep compiling while
downstream code gains real recovery branches instead of a
stringly-typed blob.
Sharing one config across 8 tokio tasks without a data race
You spawn 8 background tasks and each needs read access to
a config struct that one task occasionally updates. Search
"Arc", "RwLock", and "async". The cheat sheet shows
Arc<RwLock<Config>> cloned per task, plus the trap that
eats an afternoon: never hold a std::sync lock across an
.await. Swap to tokio::sync::RwLock and the runtime stops
starving under load.
Prepping for a Rust interview the night before
You have one evening and a screen-share interview tomorrow.
Run the search box for "iterator", "trait", "closure", and
read 30 entries with runnable code. The Fn / FnMut / FnOnce
distinction, dyn vs impl Trait, and the eight pitfalls are
exactly the topics interviewers probe. Reading code you can
mentally run beats re-skimming a 400-page book at 11pm.
Common pitfalls
Copying a snippet that uses let-else or {var} format shorthand onto an old toolchain and getting a syntax error. Check the entry's version note (let-else needs 1.65, {var} needs 1.58) before pasting.
Searching only English keywords like "lifetime" and missing entries. The box searches code and both languages, so "生命周期" or the literal token 'a both surface the same cards.
Treating the Arc<Mutex<T>> snippet as safe inside async. That example targets std::thread. Across .await you must use tokio::sync::Mutex or the runtime can deadlock.
Privacy
The whole cheat sheet is one static page and every snippet lives in an in-memory array in your browser. Your search terms never leave the tab, never hit a server, and never enter the URL. Open DevTools then Network while you type and you will see zero requests, so it is safe behind corporate proxies or on air-gapped machines.
FAQ
Related tools
Hand-picked utilities that pair well with this one.