Every value has exactly one owner. When the owner goes out of scope the value is dropped. References let you borrow a value without taking ownership — the borrow checker enforces the aliasing rules at compile time with zero runtime cost.
Move semantics
let s1 = String::from("hello"); let s2 = s1; // s1 is moved — s1 is now invalid // println!("{}", s1); // compile error: value used after move let s3 = s2.clone(); // deep copy — both s2 and s3 are valid // Types that implement Copy are duplicated automatically: let x: i32 = 5; let y = x; // x is still valid — i32 is Copy
References and borrowing rules
&T (shared) references, or exactly one &mut T (exclusive) reference — never both simultaneously.fn length(s: &String) -> usize { s.len() // borrows s; caller keeps ownership } fn append(s: &mut String) { s.push_str(" world"); } let mut msg = String::from("hello"); append(&mut msg); println!("{}", length(&msg)); // "hello world" // Slice references: lightweight view into a collection let arr = [1, 2, 3, 4, 5]; let slice: &[i32] = &arr[1..3]; // [2, 3]
Ownership in functions
// Takes ownership — caller can no longer use s fn consume(s: String) { println!("{}", s); } // Returns ownership back fn give_back(s: String) -> String { s } // Preferred: borrow rather than take/return fn process(s: &str) -> usize { s.len() }
Rust is statically typed with inference. Structs hold data; traits define shared behaviour (like interfaces, but with no inheritance). Generics let you write code that works over many concrete types without runtime overhead.
Primitive types
| Category | Types | Notes |
|---|---|---|
| Integers | i8 i16 i32 i64 i128 isizeu8 u16 u32 u64 u128 usize | Default inferred as i32. usize is pointer-sized. |
| Floats | f32 f64 | Default inferred as f64. |
| Boolean | bool | true / false |
| Character | char | Unicode scalar value (4 bytes). |
| Tuple | (i32, &str, bool) | Fixed-size, heterogeneous. Access with .0, .1… |
| Array | [i32; 5] | Fixed-size, stack-allocated. |
| Slice | &[T] | Dynamically sized view into a sequence. |
| String | String / &str | String = heap-owned. &str = borrowed string slice. |
Structs
struct Point { x: f64, y: f64 } impl Point { fn new(x: f64, y: f64) -> Self { Point { x, y } } fn distance(&self, other: &Point) -> f64 { ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt() } } // Tuple struct struct Meters(f64); // Unit struct (useful as a marker type) struct Marker;
Traits
trait Area { fn area(&self) -> f64; fn describe(&self) -> String { // default implementation format!("area = {:.2}", self.area()) } } struct Circle { radius: f64 } struct Rect { w: f64, h: f64 } impl Area for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } } impl Area for Rect { fn area(&self) -> f64 { self.w * self.h } } // Trait bounds — monomorphised (static dispatch, no overhead) fn print_area<T: Area>(shape: &T) { println!("{}", shape.describe()); } // Dynamic dispatch — erases concrete type at runtime fn print_dyn(shape: &dyn Area) { println!("{}", shape.describe()); } // Common standard traits // Debug, Display, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord #[derive(Debug, Clone, PartialEq)] struct Config { timeout_ms: u64 }
Generics
struct Stack<T> { items: Vec<T> } impl<T> Stack<T> { fn new() -> Self { Stack { items: Vec::new() } } fn push(&mut self, item: T) { self.items.push(item); } fn pop(&mut self) -> Option<T> { self.items.pop() } } // Where clauses for complex bounds fn largest<T>(list: &[T]) -> &T where T: PartialOrd { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest }
Rust enums are algebraic data types — each variant can carry different data. Pattern matching with match is exhaustive: the compiler forces you to handle every variant.
Defining enums
#[derive(Debug)] enum Message { Quit, // unit variant Move { x: i32, y: i32 }, // struct-like variant Write(String), // tuple variant Color(u8, u8, u8), // tuple variant (RGB) } impl Message { fn call(&self) { match self { Message::Quit => println!("quit"), Message::Move { x, y } => println!("move to {x},{y}"), Message::Write(text) => println!("write: {text}"), Message::Color(r,g,b) => println!("color #{r:02x}{g:02x}{b:02x}"), } } }
Option and Result
// Option<T> — value may or may not exist let some: Option<i32> = Some(42); let none: Option<i32> = None; let val = some.unwrap_or(0); // 42 let val = none.unwrap_or_default(); // 0 let val = some.map(|v| v * 2); // Some(84) // if let — match a single variant if let Some(n) = some { println!("got {n}"); } // while let let mut stack = vec![1, 2, 3]; while let Some(top) = stack.pop() { println!("{top}"); } // Result<T, E> — success or error let ok: Result<i32, &str> = Ok(1); let err: Result<i32, &str> = Err("oops"); match ok { Ok(v) => println!("success: {v}"), Err(e) => eprintln!("error: {e}"), }
Advanced patterns
let num = 7; match num { 1 => println!("one"), 2 | 3 => println!("two or three"), 4..=6 => println!("four to six"), n if n > 6 => println!("greater than six: {n}"), // guard _ => println!("other"), } // Destructuring tuples and structs let (a, b, c) = (1, 2, 3); let Point { x, y } = Point::new(3.0, 4.0); // @ bindings — capture while matching match num { n @ 1..=10 => println!("small: {n}"), n => println!("large: {n}"), }
Rust has no exceptions. Recoverable errors use Result<T, E>; unrecoverable bugs use panic!. The ? operator short-circuits on Err, propagating it to the caller.
The ? operator
use std::fs; use std::io; fn read_username() -> Result<String, io::Error> { let s = fs::read_to_string("/etc/hostname")?; // returns Err early Ok(s.trim().to_string()) } // Chain multiple fallible calls cleanly fn pipeline() -> Result<u64, io::Error> { let content = fs::read_to_string("data.txt")?; let count = content.lines().count() as u64; Ok(count) }
Custom error types
use std::fmt; #[derive(Debug)] enum AppError { Io(std::io::Error), Parse(String), } impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { AppError::Io(e) => write!(f, "IO error: {e}"), AppError::Parse(s) => write!(f, "parse error: {s}"), } } } impl std::error::Error for AppError {} // Auto-convert io::Error into AppError with From impl From<std::io::Error> for AppError { fn from(e: std::io::Error) -> Self { AppError::Io(e) } } // Now ? converts io::Error -> AppError automatically fn load(path: &str) -> Result<String, AppError> { let s = std::fs::read_to_string(path)?; // io::Error -> AppError::Io Ok(s) } // For quick prototyping — Box<dyn Error> accepts any error type fn quick() -> Result<(), Box<dyn std::error::Error>> { let _s = std::fs::read_to_string("x.txt")?; Ok(()) }
The standard collections live in std::collections. Iterators are lazy pipelines: chaining adaptors costs nothing until you collect or consume them.
Common collections
use std::collections::{HashMap, HashSet, BTreeMap, VecDeque}; // Vec — growable array let mut v: Vec<i32> = vec![1, 2, 3]; v.push(4); v.extend([5, 6]); let first = v.first(); // Option<&i32> v.retain(|&x| x % 2 == 0); // keep evens: [2, 4, 6] // HashMap — O(1) average lookup let mut scores: HashMap<String, u32> = HashMap::new(); scores.insert("Alice".to_string(), 100); scores.entry("Bob".to_string()).or_insert(0); // insert if absent let score = scores.get("Alice"); // Option<&u32> // HashSet — unique elements, O(1) membership let mut seen: HashSet<i32> = HashSet::new(); seen.insert(1); seen.insert(2); seen.insert(1); // {1, 2} // VecDeque — efficient push/pop from both ends let mut dq: VecDeque<i32> = VecDeque::new(); dq.push_back(1); dq.push_front(0); dq.pop_front(); // [1]
Iterator adaptors
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8]; // Lazy pipeline — nothing runs until collect/sum/etc. let result: Vec<i32> = numbers.iter() .filter(|&&x| x % 2 == 0) // [2, 4, 6, 8] .map(|&x| x * x) // [4, 16, 36, 64] .take(3) // [4, 16, 36] .collect(); // Aggregations let sum: i32 = numbers.iter().sum(); let max = numbers.iter().max(); // Option<&i32> let count = numbers.iter().filter(|&&x| x > 4).count(); // flat_map — flatten one level of nesting let words = vec!["hello world", "foo bar"]; let chars: Vec<&str> = words.iter().flat_map(|s| s.split(' ')).collect(); // zip — pair two iterators let pairs: Vec<_> = vec![1,2,3].into_iter().zip(["a","b","c"]).collect(); // enumerate — (index, value) pairs for (i, v) in numbers.iter().enumerate() { println!("{i}: {v}"); } // Custom iterator struct Counter { count: u32, max: u32 } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<u32> { if self.count < self.max { self.count += 1; Some(self.count) } else { None } } }
Closures capture their environment by reference or by value. Lifetimes are compile-time annotations that tell the borrow checker how long references stay valid — they never affect runtime.
Closures
// Fn trait hierarchy: // FnOnce — can be called once (captures by move or drops captured values) // FnMut — can be called multiple times with &mut self // Fn — can be called multiple times with &self let factor = 3; let multiply = |x| x * factor; // captures factor by reference (Fn) println!("{}", multiply(5)); // 15 // move closure — takes ownership of captured values let greeting = String::from("hello"); let greet = move || println!("{greeting}"); // greeting moved in // Higher-order functions fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) } fn make_adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n }
Lifetimes
// Lifetime annotations say: "the returned reference lives as long as both inputs" fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // Structs holding references need lifetime annotations struct Excerpt<'a> { part: &'a str } // 'static — lives for the entire program duration let s: &'static str = "I live forever"; // Elision rules — compiler infers lifetimes in common patterns // fn first_word(s: &str) -> &str infers fn first_word<'a>(s: &'a str) -> &'a str
Smart pointers add ownership semantics or reference counting on top of raw heap allocation. Box is the simplest; Rc/Arc enable shared ownership; RefCell/Mutex enable interior mutability.
| Type | Ownership | Thread safe | Use when |
|---|---|---|---|
Box<T> | Single | Yes (if T: Send) | Heap allocation, recursive types, trait objects |
Rc<T> | Shared | No | Multiple owners in single-threaded code |
Arc<T> | Shared | Yes | Multiple owners across threads |
Cell<T> | Single | No | Interior mutability for Copy types |
RefCell<T> | Single | No | Interior mutability with runtime borrow checks |
Mutex<T> | Shared | Yes | Exclusive mutable access across threads |
RwLock<T> | Shared | Yes | Many readers or one writer across threads |
use std::rc::Rc; use std::cell::RefCell; use std::sync::Arc; // Box: heap-allocate a value let b = Box::new(5); // Recursive type only possible with Box enum List { Cons(i32, Box<List>), Nil } // Rc: multiple owners in one thread let shared = Rc::new(vec![1, 2, 3]); let clone1 = Rc::clone(&shared); println!("refs: {}", Rc::strong_count(&shared)); // 2 // RefCell: borrow-check at runtime (panics on violation) let data = Rc::new(RefCell::new(vec![1])); data.borrow_mut().push(2); // mutate through shared Rc // Arc: Rc but atomic (cross-thread) let arc = Arc::new(vec![1, 2, 3]); let arc2 = Arc::clone(&arc); std::thread::spawn(move || println!("{:?}", arc2));
std::fs covers one-shot reads/writes. std::io::{BufReader, BufWriter} add buffering for streaming. Files implement Read and Write traits — the same interface used for sockets and in-memory buffers.
Reading files
use std::fs::{self, File}; use std::io::{self, BufRead, BufReader, Read}; // Entire file into String (small files) let content = fs::read_to_string("config.txt")?; // Entire file into Vec<u8> (binary) let bytes = fs::read("image.png")?; // Line-by-line (memory-efficient for large files) let file = File::open("data.txt")?; let reader = BufReader::new(file); for line in reader.lines() { let line = line?; println!("{line}"); } // Read into a fixed buffer let mut file = File::open("data.bin")?; let mut buf = [0u8; 4096]; loop { let n = file.read(&mut buf)?; if n == 0 { break; } // process &buf[..n] }
Writing files
use std::fs::{self, OpenOptions}; use std::io::{BufWriter, Write}; // Overwrite / create fs::write("out.txt", "hello\n")?; // Buffered write (flush on drop) let file = fs::File::create("out.txt")?; let mut writer = BufWriter::new(file); writeln!(writer, "line 1")?; writeln!(writer, "line 2")?; writer.flush()?; // Append mode let mut file = OpenOptions::new() .append(true).create(true).open("log.txt")?; writeln!(file, "new line")?; // Seek (random access) use std::io::Seek; let mut file = fs::File::open("data.bin")?; file.seek(std::io::SeekFrom::Start(64))?; // jump to byte 64
Directory and path operations
use std::path::Path; fs::create_dir_all("logs/2026")?; for entry in fs::read_dir(".")? { let entry = entry?; let path = entry.path(); if path.extension().map(|e| e == "rs").unwrap_or(false) { println!("{}", path.display()); } } let p = Path::new("/etc/hosts"); println!("{:?}", p.file_name()); // Some("hosts") println!("{:?}", p.parent()); // Some("/etc") println!("{}", p.exists()); // true/false
std::net provides synchronous TCP and UDP sockets. For production async networking, use Tokio's TcpListener/TcpStream (see the Async section). Both implement the same Read/Write traits as files.
TCP server and client (sync)
use std::net::{TcpListener, TcpStream}; use std::io::{BufRead, BufReader, Write}; use std::thread; // Server: accept connections in a loop fn run_server() -> std::io::Result<()> { let listener = TcpListener::bind("0.0.0.0:8080")?; println!("listening on :8080"); for stream in listener.incoming() { let stream = stream?; thread::spawn(move || handle(stream)); } Ok(()) } fn handle(mut stream: TcpStream) { let reader = BufReader::new(stream.try_clone().unwrap()); for line in reader.lines().flatten() { let response = format!("echo: {line}\n"); stream.write_all(response.as_bytes()).ok(); } } // Client: connect and exchange data fn run_client() -> std::io::Result<()> { let mut stream = TcpStream::connect("127.0.0.1:8080")?; stream.write_all(b"hello\n")?; let mut resp = String::new(); BufReader::new(&stream).read_line(&mut resp)?; println!("server said: {resp}"); Ok(()) }
UDP socket
use std::net::UdpSocket; // Sender let sock = UdpSocket::bind("0.0.0.0:0")?; // OS picks port sock.send_to(b"ping", "127.0.0.1:9000")?; // Receiver let sock = UdpSocket::bind("0.0.0.0:9000")?; let mut buf = [0u8; 1024]; let (n, src) = sock.recv_from(&mut buf)?; println!("from {src}: {:?}", &buf[..n]); // Set timeouts to avoid blocking forever use std::time::Duration; sock.set_read_timeout(Some(Duration::from_secs(5)))?;
Socket options
use std::time::Duration; stream.set_nodelay(true)?; // disable Nagle stream.set_read_timeout(Some(Duration::from_secs(30)))?; stream.set_write_timeout(Some(Duration::from_secs(30)))?; stream.set_ttl(64)?; let addr = stream.peer_addr()?;
Rust's ownership system makes data-race freedom a compile-time guarantee: a value can only be sent to another thread if it implements Send; a shared reference can only cross a thread boundary if T: Sync. Closures capture their environment, so move closures are the standard way to hand off data.
Spawning threads
use std::thread; use std::time::Duration; // Spawn and join let handle = thread::spawn(|| { println!("running in thread {:?}", thread::current().id()); 42 }); let result = handle.join().unwrap(); // blocks until thread finishes; returns 42 // move closure — take ownership of data before crossing thread boundary let data = vec![1, 2, 3]; let h = thread::spawn(move || { println!("{:?}", data); // data moved into thread }); h.join().unwrap(); // Thread builder: name and stack size let h = thread::Builder::new() .name("worker-1".into()) .stack_size(4 * 1024 * 1024) .spawn(|| { /* ... */ })?; h.join().unwrap(); // Thread-local storage thread_local! { static COUNTER: std::cell::Cell<u32> = std::cell::Cell::new(0); } COUNTER.with(|c| c.set(c.get() + 1));
Scoped threads (std::thread::scope)
// Scoped threads can borrow from the outer stack frame safely let data = vec![1, 2, 3, 4]; thread::scope(|s| { s.spawn(|| println!("first half: {:?}", &data[..2])); s.spawn(|| println!("second half: {:?}", &data[2..])); }); // All threads joined here — data is still accessible below println!("all done, data len = {}", data.len());
Mutex<T> and RwLock<T> wrap data and only let you access it after acquiring the lock. The lock guard is released automatically when it goes out of scope — no risk of forgetting to unlock. Wrap in Arc to share across threads.
Mutex
use std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0_u64)); let handles: Vec<_> = (0..8).map(|_| { let c = Arc::clone(&counter); thread::spawn(move || { let mut guard = c.lock().unwrap(); // blocks until acquired *guard += 1; // guard dropped here — lock released }) }).collect(); for h in handles { h.join().unwrap(); } println!("counter = {}", *counter.lock().unwrap()); // 8 // Poisoning: if a thread panics while holding the lock, // subsequent lock() calls return Err(PoisonError). Recover with: let guard = counter.lock().unwrap_or_else(|e| e.into_inner());
RwLock — many readers or one writer
use std::sync::{Arc, RwLock}; let cache: Arc<RwLock<HashMap<String, String>>> = Arc::new(RwLock::new(HashMap::new())); // Multiple concurrent readers let r = cache.read().unwrap(); println!("{:?}", r.get("key")); drop(r); // release read lock before taking write lock // Exclusive writer let mut w = cache.write().unwrap(); w.insert("key".to_string(), "value".to_string());
Atomics — lock-free primitives
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; let count = Arc::new(AtomicUsize::new(0)); let c = Arc::clone(&count); thread::spawn(move || { c.fetch_add(1, Ordering::SeqCst); }); // Ordering guide: // Relaxed — no synchronisation, just atomicity (counters, stats) // Acquire — read side of a happens-before relationship // Release — write side of a happens-before relationship // SeqCst — total sequential order (safest, most expensive) // AtomicBool is common for stop-flags let running = Arc::new(std::sync::atomic::AtomicBool::new(true)); let r = Arc::clone(&running); thread::spawn(move || { while r.load(Ordering::Acquire) { /* work */ } }); running.store(false, Ordering::Release);
Condvar — wait for a condition
use std::sync::{Arc, Condvar, Mutex}; let pair = Arc::new((Mutex::new(false), Condvar::new())); let pair2 = Arc::clone(&pair); thread::spawn(move || { let (lock, cvar) = &*pair2; let mut ready = lock.lock().unwrap(); *ready = true; cvar.notify_one(); }); let (lock, cvar) = &*pair; let mut ready = lock.lock().unwrap(); while !*ready { ready = cvar.wait(ready).unwrap(); } println!("signalled!");
std::sync::mpsc (multiple-producer, single-consumer) is the standard way to pass ownership of data between threads without shared state. For multi-consumer or bounded patterns, the crossbeam-channel or flume crates provide richer options.
mpsc — unbounded channel
use std::sync::mpsc; use std::thread; let (tx, rx) = mpsc::channel::<String>(); // Multiple producers from cloned senders for i in 0..4 { let tx = tx.clone(); thread::spawn(move || { tx.send(format!("message from thread {i}")).unwrap(); }); } drop(tx); // drop the original sender so receiver knows when all senders are gone // Consumer drains until all senders dropped for msg in rx { println!("{msg}"); } // Non-blocking try_recv match rx.try_recv() { Ok(msg) => println!("{msg}"), Err(mpsc::TryRecvError::Empty) => { /* nothing yet */ } Err(mpsc::TryRecvError::Disconnected) => { /* all senders gone */ } }
mpsc — sync (bounded) channel
// sync_channel(n): sender blocks when buffer is full (back-pressure) let (tx, rx) = mpsc::sync_channel::<u32>(16); // capacity = 16 thread::spawn(move || { for i in 0..100 { tx.send(i).unwrap(); // blocks when buffer full } }); for val in rx { print!("{val} "); } // Worker pool pattern: one channel, N worker threads let (tx, rx) = mpsc::channel::<u32>(); let rx = Arc::new(Mutex::new(rx)); for _ in 0..4 { let rx = Arc::clone(&rx); thread::spawn(move || { loop { let job = rx.lock().unwrap().recv(); match job { Ok(n) => println!("worker processing {n}"), Err(_) => break, // sender disconnected } } }); }
Rust's async/await syntax produces state machines that are polled to completion by an executor. Tokio is the de-facto async runtime — it provides an I/O reactor, a work-stealing thread pool, timers, and async-native TCP/UDP/file utilities.
tokio = { version = "1", features = ["full"] }The basics
use tokio::time::{sleep, Duration}; // async fn returns a Future — it runs when awaited async fn fetch(id: u32) -> String { sleep(Duration::from_millis(100)).await; // yields, doesn't block thread format!("result for {id}") } // #[tokio::main] creates the runtime and runs async main #[tokio::main] async fn main() { let result = fetch(1).await; println!("{result}"); } // Run futures concurrently with join! let (a, b) = tokio::join!(fetch(1), fetch(2)); // Spawn onto the executor (like thread::spawn but async) let task = tokio::spawn(async { fetch(3).await }); let value = task.await.unwrap();
Async TCP server
use tokio::net::{TcpListener, TcpStream}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; #[tokio::main] async fn main() -> tokio::io::Result<()> { let listener = TcpListener::bind("0.0.0.0:8080").await?; loop { let (stream, addr) = listener.accept().await?; println!("connection from {addr}"); tokio::spawn(async move { handle(stream).await }); } } async fn handle(mut stream: TcpStream) { let (reader, mut writer) = stream.split(); let mut lines = BufReader::new(reader).lines(); while let Ok(Some(line)) = lines.next_line().await { let resp = format!("echo: {line}\n"); writer.write_all(resp.as_bytes()).await.ok(); } }
Async channels and select!
use tokio::sync::{mpsc, broadcast}; use tokio::time::{sleep, Duration}; // mpsc — async multiple-producer single-consumer let (tx, mut rx) = mpsc::channel::<u32>(32); tokio::spawn(async move { tx.send(1).await.unwrap(); }); while let Some(v) = rx.recv().await { println!("{v}"); } // broadcast — one sender, many receivers (each gets every message) let (btx, mut brx) = broadcast::channel::<u32>(16); let mut brx2 = btx.subscribe(); btx.send(42).unwrap(); // select! — race multiple async branches tokio::select! { val = rx.recv() => println!("channel: {val:?}"), _ = sleep(Duration::from_secs(1)) => println!("timeout"), }
Shared state in async code
use tokio::sync::{Mutex, RwLock}; use std::sync::Arc; // tokio::sync::Mutex — async-aware, holds lock across .await let state = Arc::new(Mutex::new(Vec::<u32>::new())); let s = Arc::clone(&state); tokio::spawn(async move { let mut guard = s.lock().await; guard.push(1); }); // Prefer std::sync::Mutex when the lock is never held across .await // (avoids the async overhead for synchronous critical sections) // OnceLock / LazyLock for one-time async initialisation use tokio::sync::OnceCell; static CONFIG: OnceCell<String> = OnceCell::const_new(); let cfg = CONFIG.get_or_init(|| async { "loaded".to_string() }).await;
Ownership quick rules
Move by default for non-Copy types
&T — many shared borrows
&mut T — one exclusive borrow
clone() — explicit deep copy
Value dropped when owner leaves scope
Error propagation
? — propagate Err/None up
unwrap() — panic on Err/None
unwrap_or(v) — default value
map_err(|e| ...) — convert error
anyhow::Result — app errors
thiserror — library errors
Common traits to implement
Display — user-facing formatting
Debug — derive, for {:?}
From / Into — conversions
Iterator — custom iterators
Default — zero value
Drop — custom destructor
Concurrency patterns
Arc<Mutex<T>> — shared mutable state
Arc<RwLock<T>> — read-heavy shared state
mpsc::channel — pass data between threads
AtomicBool — stop flags
Prefer channels over shared state
File I/O patterns
fs::read_to_string() — small text files
BufReader::lines() — large text files
BufWriter — batch writes
OpenOptions — append / create flags
fs::read() — binary files
Async rules of thumb
tokio::spawn — background task
tokio::join! — concurrent, same scope
tokio::select! — race futures
Don't block inside async — use spawn_blocking
Use tokio::sync::Mutex if held across .await
Smart pointer decision tree
Single owner → Box<T>
Shared, one thread → Rc<T>
Shared, multi-thread → Arc<T>
Interior mutability (single) → RefCell<T>
Interior mutability (multi) → Mutex<T> / RwLock<T>
Cargo essentials
cargo new <name> — new project
cargo build --release — optimised build
cargo test — run tests
cargo clippy — lint
cargo doc --open — browse docs
cargo add <crate> — add dependency
Type conversions at a glance
| From | To | How |
|---|---|---|
&str | String | s.to_string() or String::from(s) |
String | &str | &s or s.as_str() |
i32 | String | n.to_string() |
&str | i32 | s.parse::<i32>()? |
Vec<T> | &[T] | &v or v.as_slice() |
&[T] | Vec<T> | slice.to_vec() |
Option<T> | Result<T, E> | opt.ok_or(err) |
Result<T, E> | Option<T> | result.ok() |
Iterator cheat sheet
| Adaptor | What it does |
|---|---|
map(f) | Transform each element |
filter(pred) | Keep elements matching predicate |
filter_map(f) | Map then discard None |
flat_map(f) | Map then flatten one level |
take(n) / skip(n) | Limit / skip first n elements |
enumerate() | Pair each element with its index |
zip(other) | Pair elements from two iterators |
chain(other) | Concatenate two iterators |
fold(init, f) | Accumulate into a single value |
any(pred) / all(pred) | Short-circuit boolean checks |
collect::<C>() | Consume into a collection |
sum() / product() | Numeric aggregations |
min() / max() | Smallest / largest element (Option) |
count() | Number of elements |
peekable() | Look at next element without consuming |