English | 繁體中文
Rust — one-page reference
Rust is a compiled, statically typed systems language focused on memory safety without garbage collection. Ownership, borrowing, lifetimes, algebraic data types, traits, and exhaustive pattern matching make invalid states difficult to represent. Prefer cargo fmt, cargo clippy, tests, and small explicit abstractions; let the compiler explain the next safe change.
Toolchain and Cargo
rustup toolchain install stable # install/update a toolchain
rustup default stable
cargo new hello # binary crate
cargo new --lib library # library crate
cargo add serde --features derive # add a dependency (cargo-edit)
cargo check # type-check quickly
cargo build --release # optimized build in target/release
cargo run --release -- arg1 # build and run
cargo test # unit and integration tests
cargo test -- --nocapture # show test output
cargo fmt -- --check # verify canonical formatting
cargo clippy --all-targets --all-features -- -D warnings
cargo doc --open # build API documentation
cargo tree # inspect dependency graph
rustc --edition 2021 main.rs # compile a standalone fileA package contains Cargo.toml and one or more crates. src/main.rs is a binary; src/lib.rs is a library. Use workspaces for related packages. Keep public APIs small, document public items, pin or review dependency updates, and commit Cargo.lock for applications (libraries may omit it).
Syntax, bindings, and core types
fn main() {
let answer = 42; // immutable by default; inferred as i32
let mut count: u64 = 0; // mutation must be explicit
count += 1;
const MAX_RETRIES: usize = 3; // compile-time constant
let shadow = "text";
let shadow = shadow.len(); // shadowing can change the type
println!("{answer} {count} {MAX_RETRIES} {shadow}");
}
// Scalar types: bool, char (Unicode scalar value), signed/unsigned integers,
// f32/f64. Compound types: tuples and fixed-size arrays.
let pair: (i32, &str) = (7, "seven");
let (number, word) = pair; // destructuring pattern
let bytes: [u8; 3] = [1, 2, 3];
let repeated = [0; 4];
let slice: &[u8] = &bytes[1..]; // borrowed view; no ownership
// Collections own heap data and are usually mutable through `let mut`.
let mut text = String::from("Rust");
text.push_str("!\n");
let mut values = vec![1, 2, 3];
values.push(4);
let map = std::collections::HashMap::from([("rust", 2010)]);
// Conversions are explicit: `as` for primitive casts; `parse` for strings.
let n: u32 = "42".parse().expect("valid number");
let widened = n as u64;Expressions return values; statements end in ;. The last expression in a block is its result. if, match, and loops are expressions. There is no implicit truthiness: conditions must be bool.
let label = if count > 0 { "non-empty" } else { "empty" };
for value in [10, 20, 30] { println!("{value}"); }
for i in 0..3 { println!("{i}"); } // 0, 1, 2
for i in (0..=3).rev() { println!("{i}"); }
let mut n = 0;
while n < 3 { n += 1; }
let result = loop {
n += 1;
if n == 5 { break n * 2; }
};
'outer: for row in 0..3 {
for col in 0..3 {
if row == col { break 'outer; }
}
}Ownership, moves, copies, and borrowing
Every value has one owner. There is exactly one owner at a time; when the owner leaves scope, its value is dropped. Assignment, argument passing, and returning normally move ownership. Types implementing Copy (numbers, bool, char, shared immutable references, and small tuples of Copy values) are copied instead.
fn take(text: String) -> usize { text.len() } // consumes the String
fn borrow(text: &str) -> usize { text.len() } // shared borrow; does not consume
fn append(text: &mut String) { text.push('!') } // exclusive borrow
let original = String::from("hello");
let moved = original; // String moved; `original` is no longer usable
let length = borrow(&moved); // shared borrow
let mut owned = moved;
append(&mut owned); // exclusive borrow
println!("{owned} ({length})");
let a = 10;
let b = a; // i32 is Copy; both remain usableReferences cannot outlive the value they reference. Rust prevents dangling references and data races at compile time. At one time, a value may have many immutable (&T) borrows or one mutable (&mut T) borrow, but not both; borrows must also be valid for their use.
fn first_word(text: &str) -> &str {
text.split_whitespace().next().unwrap_or("")
}
let mut name = String::from("Ada Lovelace");
let first = first_word(&name); // immutable borrow ends after last use
println!("{first}");
name.push_str(" (1815)"); // now a mutable borrow is allowedUse &str for borrowed text and String for owned, growable text. &[T] is a borrowed slice; Vec<T> owns a growable buffer. Box<T> owns a heap allocation, Rc<T> provides single-threaded shared ownership, and Arc<T> provides thread-safe reference counting. RefCell<T>/Mutex<T> move some borrowing checks to runtime; failed rules may panic or poison instead of becoming compile errors.
Structs, enums, methods, and patterns
#[derive(Debug, Clone, PartialEq, Eq)]
struct User {
id: u64,
name: String,
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Text(String),
ChangeColor(u8, u8, u8),
}
impl User {
fn label(&self) -> String { format!("{}: {}", self.id, self.name) }
fn rename(&mut self, name: impl Into<String>) { self.name = name.into(); }
}
fn describe(message: Message) -> String {
match message {
Message::Quit => "quit".into(),
Message::Move { x, y } => format!("move to ({x}, {y})"),
Message::Text(text) if text.is_empty() => "empty text".into(),
Message::Text(text) => text,
Message::ChangeColor(r, g, b) => format!("rgb({r}, {g}, {b})"),
}
}
let mut user = User { id: 1, name: "Ada".into() };
user.rename("Grace");
let User { id, ref name } = user; // `ref` borrows a field in a pattern
println!("{id}: {name}");
if let Some(value) = Some(3) { println!("{value}"); }
let status = match 404 {
200..=299 => "success", // inclusive range pattern
400 | 404 => "client error", // or-pattern
_ => "other", // wildcard; match must be exhaustive
};Patterns appear in let, match, if let, while let, for, and function parameters. Use ref, @, .., ranges, guards (if condition), and matches! when useful. Option<T> represents presence (Some) or absence (None); never use null. Result<T, E> represents success or failure.
Functions, generics, traits, and lifetimes
fn sum<T>(items: impl IntoIterator<Item = T>) -> T
where
T: std::ops::Add<Output = T> + Default,
{
items.into_iter().fold(T::default(), |total, item| total + item)
}
trait Summarize {
fn summary(&self) -> String;
fn short(&self) -> String { self.summary().chars().take(20).collect() }
}
impl Summarize for User {
fn summary(&self) -> String { self.label() }
}
fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
if left.len() >= right.len() { left } else { right }
}Generics are monomorphized by default, giving static dispatch and no inherent runtime cost. Trait bounds constrain capabilities. impl Trait is convenient for arguments and opaque return types; dyn Trait is a heap-backed trait object for runtime dispatch, e.g. Box<dyn Error + Send + Sync>. Lifetimes describe relationships between references; they do not extend an underlying value’s lifetime. Most lifetimes are inferred. Use 'static only when a value owns its data or truly lives for the entire program.
Closures capture by shared borrow, mutable borrow, or move. move forces captured ownership and is common when spawning threads. Iterator adapters (map, filter, zip, take) are lazy; terminal methods such as collect, sum, for_each, and fold execute them.
let doubled: Vec<_> = [1, 2, 3, 4]
.into_iter()
.filter(|n| n % 2 == 0)
.map(|n| n * 2)
.collect();Errors, ?, and resource safety
Return Result<T, E> for recoverable errors and Option<T> for expected absence. ? returns early on failure, converting compatible errors via From. Use thiserror for typed library errors and anyhow for application-level context when external crates are appropriate. Drop runs automatically at scope exit; use RAII guards for cleanup and lock release. panic! is for violated invariants or unrecoverable bugs, not routine input validation.
use std::{fs, io, path::Path};
fn read_config(path: &Path) -> Result<String, io::Error> {
let text = fs::read_to_string(path)?;
if text.trim().is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "empty config"));
}
Ok(text)
}Avoid unwrap/expect on untrusted input or production paths; they are suitable when an invariant is locally obvious and failure should abort. Never discard a Result accidentally. Validate paths and external data at boundaries; avoid building shell commands or SQL with string concatenation.
Concurrency and async
Rust’s Send and Sync traits make thread-safety part of the type system. std::thread::spawn requires captured values to satisfy 'static; move transfers ownership into the thread. Join handles propagate panics as Result. Use channels for ownership transfer and Arc<Mutex<T>>/Arc<RwLock<T>> for shared mutable state. Prefer message passing or immutable data to broad locking. Atomics are for small lock-free state with an explicit ordering model.
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
fn main() {
let (jobs_tx, jobs_rx) = mpsc::channel::<u32>();
let jobs_rx = Arc::new(Mutex::new(jobs_rx));
let mut workers = Vec::new();
for _ in 0..2 {
let jobs_rx = Arc::clone(&jobs_rx);
workers.push(thread::spawn(move || {
let mut total = 0;
loop {
let job = jobs_rx.lock().expect("receiver lock").recv();
match job {
Ok(value) => total += value * value,
Err(_) => return total,
}
}
}));
}
for value in 1..=4 { jobs_tx.send(value).expect("workers are alive"); }
drop(jobs_tx); // close the work stream
let total: u32 = workers.into_iter()
.map(|worker| worker.join().expect("worker did not panic"))
.sum();
println!("total={total}");
}The standard library has threads, channels, Mutex, RwLock, Condvar, OnceLock, and atomics. rayon is useful for CPU-bound data parallelism. async fn returns a Future; it does not run until awaited by an executor such as Tokio or async-std. Async tasks are cooperative: avoid blocking calls on an async runtime thread; use runtime-aware I/O and bounded concurrency. Send permits moving a future between threads; Sync permits sharing references. Cancellation is usually represented by dropping a future or using an explicit token/channel.
Modules, macros, unsafe, and FFI
mod geometry {
pub struct Point { pub x: f64, pub y: f64 }
pub fn distance(a: Point, b: Point) -> f64 {
((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt()
}
}
use geometry::Point;Modules are private by default; pub exposes items, pub(crate) exposes them within the crate. crate::, self::, and super:: are useful absolute/relative paths. Declarative macros use macro_rules!; derive/procedural macros generate code (#[derive(Debug)], #[test]).
unsafe is not a performance switch: it permits raw-pointer dereference, calling unsafe functions, mutable static access, union field access, and implementing unsafe traits. Keep unsafe blocks tiny, document the invariant that makes them sound, wrap them in a safe API, and run Miri/sanitizers where practical. FFI boundaries require #[repr(C)], correct ownership/layout, and explicit handling of null pointers and error codes.
Standard-library map
- Collections:
Vec,VecDeque,LinkedList(rare),HashMap,HashSet,BTreeMap,BTreeSet,BinaryHeap. - Text/data:
String,str,char,std::fmt,std::str,std::convert,serdeexternally for JSON/serialization. - Files/processes:
std::fs,std::path,std::io,std::env,std::process,std::ffi. - Time/networking:
std::time,std::net; usereqwest/Tokio or equivalent for production HTTP clients. - Concurrency:
std::thread,std::sync,std::sync::atomic; usecrossbeamfor richer scoped channels/threads. - Iteration:
Iterator,IntoIterator,iter,iter_mut,into_iter,FromIterator. - Inspection:
Debug({:?}),Display({}),assert!,dbg!,std::backtrace.
Testing, performance, and design checklist
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn longest_returns_the_longer_slice() {
assert_eq!(longest("short", "longer"), "longer");
}
}Use unit tests beside the code, integration tests in tests/, doctests in documentation, and property/fuzz tests with proptest or cargo-fuzz when useful. Run cargo test, cargo clippy, and cargo fmt -- --check in CI. Benchmark with criterion instead of guessing; profile before optimizing. Prefer iterators when they clarify intent, but measure allocations and hot paths. Use cargo audit/cargo deny for dependency hygiene and cargo +nightly miri test for certain undefined-behavior checks.
Idioms: make invalid states unrepresentable with enums/newtypes; return Result instead of sentinel values; borrow when a function need not own; accept impl AsRef<str>/slices where it improves APIs; use &[T] rather than &Vec<T>; derive standard traits deliberately; keep unsafe isolated; avoid needless clone; use Arc/Mutex only when ownership design requires shared mutation; document invariants and public safety contracts; prefer exhaustive matches; make error context actionable.