Open-source libraries used

1 libraries are bundled into this tool's code.

Rust Cheatsheet — Quick Reference

A concise Rust 2021 cheatsheet covering syntax, ownership, error handling and the most common std APIs — about 80% of everyday scenarios.

Rs

Rust Rust 2021 edition

rustc / cargo · Compiled · systems · functional · concurrent · Static · strongly typed · affine (move semantics)

Recommended Learning Path

First learn cargo new / run and println! → master variable bindings, ownership and borrowing (the Rust core) → dive into match, functions and closures → organise data with Vec / HashMap / iterators → understand traits and generics → handle errors with Result and ? → write concurrency with threads and channel → then learn networking, time and build/test as needed. The FAQ section is great for coming back to avoid pitfalls.

1.Hello World & Build Environment

Run Rust programs, cargo projects, and the toolchain.

Minimal program

fn main is the entry point. The println! macro prints output. Statements end with a semicolon.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Create a new project:
// cargo new hello
// cd hello
// Edit src/main.rs:
fn main() {
println!("Hello, world!");
}
// Run:
// cargo run
// Output: Hello, world!
// Key points:
// fn keyword defines a function
// println! is a macro (with !)
// main returns type ()
// Trailing semicolon is optional on expressions (see Functions section)

Run & build

cargo run compiles and runs, cargo build produces binaries, cargo check is a fast type-check.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Compile and run:
// cargo run
// Build (debug):
// cargo build
// Quick check (no binary output):
// cargo check
// Release build (optimized):
// cargo build --release
// Artifact location:
// target/debug/hello
// target/release/hello
// Compile a single file directly:
// rustc main.rs
// Generates executable main
// Always use cargo for large projects

cargo project

cargo new initializes a project. Cargo.toml declares the package and dependencies. src/ holds source.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Create a binary project:
// cargo new hello --bin
// Create a library project:
// cargo new mylib --lib
// Cargo.toml essentials:
// [package]
// name = "hello"
// version = "0.1.0"
// edition = "2021"
// Layout:
// Cargo.toml manifest
// src/main.rs entry point
// src/lib.rs library entry point
// target/ build artifacts
// Generate docs:
// cargo doc --open
// Build cache target/ can be deleted

edition

edition declares the language edition. 2021 is current. rust-version declares the minimum toolchain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Cargo.toml:
// [package]
// name = "app"
// version = "0.1.0"
// edition = "2021" // language edition
// rust-version = "1.70" // minimum rustc
// Check current version:
// rustc --version
// Upgrade edition:
// cargo fix --edition
// Editions:
// 2015 (old) / 2018 / 2021
// Edition differences:
// Keyword availability
// Macro and prelude differences
// A project uses only one edition at a time

Output macros

println! / print! for stdout, eprintln! for stderr, format! to build a String.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let name = "Rust";
let ver = 1.82;
// Placeholder:
println!("Hello, {}!", name);
println!("version: {}", ver);
// Numbered and named:
println!("{0} {1} {0}", "a", "b");
println!("{name} v{ver}", name = name, ver = ver);
// Format control:
println!("{:>10}", "right"); // width
println!("{:.2}", 3.14159); // precision
println!("{:b}", 10); // binary
println!("{:?}", vec![1, 2]); // Debug
// No newline:
print!("continued");
// Error stream:
eprintln!("an error occurred");
}

Command-line arguments

std::env::args returns CLI arguments. The first one is the program path.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::env;
fn main() {
// Collect arguments:
let args: Vec<String> = env::args().collect();
println!("total {} arguments", args.len());
for a in &args {
println!("{}", a);
}
// Business arguments start at index 1:
// args[0] is the program path
// For non-UTF-8 use args_os:
// env::args_os()
// Common usage:
// let name = args.get(1).unwrap();
// Use clap for complex parsing

Multiple files

mod declares a module. Submodule file or directory. use imports it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/main.rs:
mod math; // declare module
use math::add;
fn main() {
println!("{}", add(1, 2));
}
// src/math.rs:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Or a directory:
// src/math/mod.rs
// src/math/utils.rs
// Export:
// pub fn / pub struct
// Private by default:
// same module can access
// child modules use super:: to refer to parent

Direct compilation

rustc compiles a single file. No cargo dependency. Good for exercises and small scripts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Compile:
// rustc hello.rs
// Run:
// ./hello
// With output name:
// rustc hello.rs -o app
// Optimize:
// rustc -O hello.rs
// Debug info:
// rustc -g hello.rs
// All warnings as errors:
// rustc -D warnings hello.rs
// Check version:
// rustc --version
// Limitations:
// no dependency management
// crates outside std
// need cargo to handle
// Suitable for teaching and single-file experiments

2.Variables & Bindings

let bindings, mutability, shadowing, scope and types.

let & mut

let bindings are immutable by default. mut makes a binding mutable. Rust variables are immutable by default.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// Immutable binding:
let x = 5;
// x = 6; // Error: immutable
// Mutable binding:
let mut y = 5;
y = 6; // allowed
println!("{}", y);
// Explicit type:
let n: u32 = 42;
// Declare first, assign later:
let mut z;
z = 10;
// Unused warning:
// prefix with _ to ignore
let _unused = 0;
}

Shadowing

A same-named let shadows the previous binding. Can change type. Different from mut.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let x = 5;
let x = x + 1; // shadowing
let x = x * 2;
println!("{}", x); // 12
// Change type:
let spaces = " ";
let spaces = spaces.len(); // usize
// Difference from mut:
// shadowing creates a new binding
// mut modifies the original value
// after shadowing, the old value is inaccessible
// Shadowing within a scope:
{
let x = 99; // inner shadowing
println!("{}", x); // 99
}
println!("{}", x); // 12
}

const & static

const is a compile-time constant. static is a static variable. Use SCREAMING_SNAKE names.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Constant:
const MAX_SIZE: u32 = 100;
const PI: f64 = 3.141592;
// Static variable (fixed address):
static APP_NAME: &str = "demo";
// Mutable static (unsafe):
static mut COUNTER: u32 = 0;
fn inc() {
unsafe {
COUNTER += 1; // requires unsafe
}
}
// Differences:
// const is inlined at compile time, no address
// static has a fixed memory address
// accessing static mut requires unsafe
// Modern practice:
// prefer const
// use a mutex for global state
// constants can be constant expressions

Type inference

The compiler infers types from context. Annotate explicitly when needed. Integers default to i32.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
// Auto inference:
let x = 42; // i32 (default)
let y = 3.14; // f64 (default)
// Context inference:
let mut v = Vec::new();
v.push(1); // infers Vec<i32>
// Explicit annotation:
let n: u64 = 42;
let f: f32 = 3.14;
// Method return fixes type:
let s: u32 = "42".parse().unwrap();
// inference fails without annotation
// integer default is i32
// float default is f64

Destructuring bindings

let supports pattern destructuring. Tuples, structs, and arrays can be unpacked at once.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// Tuple destructuring:
let (a, b, c) = (1, 2, 3);
println!("{} {} {}", a, b, c);
// Array destructuring:
let [x, y, _] = [10, 20, 30];
// Struct destructuring:
struct Point { x: i32, y: i32 }
let p = Point { x: 1, y: 2 };
let Point { x, y } = p;
// Rename:
let Point { x: px, y: py } = p;
// Rest:
let (first, rest..) = (1, 2, 3, 4);
// _ wildcard to ignore:
let (_, b2) = (1, 2);
}

Scope

Block scope with {}. Variable lifetimes. Inner scope can read outer bindings.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let outer = 1;
{
// Inner scope can read outer:
println!("{}", outer);
let inner = 2;
println!("{}", inner);
}
// println!("{}", inner); // Error: out of scope
// Same name not allowed in same scope:
// shadowing requires nested scope
// ownership ends with the scope
// Drop order (reverse):
// Drop runs at end of scope
// Call drop() to end early

Naming conventions

Rust naming conventions. snake_case for variables/functions, CamelCase for types.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Variables/functions: snake_case
let user_name = "nick";
fn get_user() {}
// Types/enums/traits: CamelCase
struct UserAccount {}
enum ColorChoice {}
trait Displayable {}
// Constants/statics: SCREAMING_SNAKE
const MAX_ITEMS: usize = 100;
// Private identifiers may use _ prefix
// Generic params: single uppercase letter T
// Lifetimes: 'a 'b
// Built-in conventions:
// is_ / has_ prefix for boolean functions
// into_ / to_ / as_ for conversions
// Compiler lints check naming

Type conversions

as performs explicit numeric conversions. From/Into trait provide safe conversions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
// as numeric conversion:
let i: i32 = 42;
let u: u64 = i as u64;
let f: f64 = i as f64;
// Truncation risk:
let big: u16 = 300;
let small: u8 = big as u8; // 44
// From/Into:
let s = String::from("hello");
let s2: String = "hi".into();
// Precise conversion:
let x: u8 = 255;
let y: u32 = x.into(); // From<u8> for u32
// Parse string:
let n: i32 = "42".parse().unwrap();
// Force type:
// let n: i32 = "42".parse()?;

3.Type System

Primitive types, tuples, structs, and enums.

Primitive types

Integers, floating-point, bool, char. Scalar types and their widths.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// Integers:
let i: i32 = -10; // signed
let u: u32 = 10; // unsigned
// Floats:
let f: f32 = 1.5;
let d: f64 = 3.14159;
// Booleans:
let b: bool = true;
let c: bool = false;
// Character (Unicode):
let ch: char = '中';
let emoji: char = '🚀';
// String slice:
let s: &str = "text";
// Integer default i32, float default f64
// char is 4 bytes (Unicode scalar)

Integer types

i8-u128, usize/isize. Signed and unsigned. Platform-dependent sizes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// Signed: i8 i16 i32 i64 i128
// Unsigned: u8 u16 u32 u64 u128
// Platform-dependent: usize isize
let a: i8 = -128; // range -128..127
let b: u8 = 255; // range 0..255
let c: i64 = 9_223_372_036_854_775_807;
// Literal suffix:
let d = 255u8;
let e = 1_000_000i64; // underscore separator
// Hex/binary:
let h = 0xff;
let bin = 0b1010;
// usize use:
// indices, lengths, pointer-sized
// overflow check (panics in debug build)

Floating-point numbers

f32 / f64 conform to IEEE 754. NaN, infinity, and arithmetic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
let a: f64 = 3.14;
let b: f64 = 2.0;
// Operations:
let sum = a + b;
let div = a / b;
let pow = a.powi(2); // power
let sqrt = a.sqrt();
// Special values:
let inf = f64::INFINITY;
let neg_inf = f64::NEG_INFINITY;
let nan = f64::NAN;
// Checks:
// nan.is_nan()
// inf.is_infinite()
// Notes:
// NaN != NaN
// floats are not suited for exact equality
// Precision:
// f32 about 7 decimal digits
// f64 about 15-16 digits

char type

char is a Unicode scalar value, 4 bytes. Use single-quote literals.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let c = 'A';
let zh = '中';
let emoji = '😀';
// Escape:
let tab = '\t';
let quote = '\'';
let newline = '\n';
// Unicode codepoint:
let cp = '\u{1F600}'; // 😀
// Operations:
println!("{}", c.is_ascii());
println!("{}", zh.is_alphabetic());
// And numbers:
let n = '9' as u8; // 57
// char and String:
// char to string:
let s = c.to_string();
// a char is not a &str

Tuples

Fixed-length heterogeneous collection. Indexed access. Destructuring. The empty tuple is ().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
// Definition:
let t = (1, "two", 3.0);
// Indexed access:
println!("{}", t.0); // 1
println!("{}", t.1); // "two"
// Type annotation:
let t2: (i32, bool) = (5, true);
// Destructuring:
let (a, b, c) = t;
// Single-element tuple:
let one = (42,); // note the comma
// Empty tuple / unit type:
let u: () = ();
// () is the default function return
// Common uses:
// multi-value return (val, err)
// swap: (a, b) = (b, a)

Structs

struct defines named fields. Construction, field access, and update syntax.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct User {
name: String,
age: u8,
active: bool,
}
fn main() {
// Instantiate:
let mut u = User {
name: String::from("nick"),
age: 30,
active: true,
};
// Access field:
println!("{}", u.name);
u.age += 1; // requires mut
// Update syntax:
let u2 = User { age: 20, ..u };
// Tuple struct:
struct Point(i32, i32);
let p = Point(1, 2);
println!("{}", p.0);
// Unit struct:
struct Unit;
}

Enums

enum defines multiple variants. Variants may carry data. match exhaustively handles them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum Message {
Quit,
Move { x: i32, y: i32 }, // struct variant
Write(String), // tuple variant
ChangeColor(u8, u8, u8),
}
fn main() {
let m = Message::Write(String::from("hi"));
// exhaustive match:
match m {
Message::Quit => println!("quit"),
Message::Move { x, y } => {
println!("move {} {}", x, y);
}
Message::Write(s) => println!("{}", s),
Message::ChangeColor(r, g, b) => {
println!("color {}-{}-{}", r, g, b);
}
}
// Enums can have generics and methods
// Option/Result are also enums

Unit type

() means no value. Functions return () by default. The convention for no-return no-arg.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// () type:
let unit: () = ();
// Functions default to returning ():
fn say() {}
// Equivalent:
fn say2() -> () {}
// Expression returning ():
let r = { let x = 1; }; // empty block
// Uses:
// as a type parameter:
// Result<T, ()> only cares about errors
// HashMap<K, ()> as a set
// Generic placeholder:
// when the concrete type doesn't matter
// Debug print:
println!("{:?}", ());

4.Ownership & References

Ownership, borrowing, references, and slices — the core of Rust memory safety.

Ownership

Each value has a single owner. Dropped automatically at end of scope. Moves rather than copies.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// Ownership rules:
// 1. Each value has exactly one owner
// 2. When the owner goes out of scope, the value is dropped
// 3. Transferring ownership is called a move
let s1 = String::from("hello");
let s2 = s1; // move: s1 is invalidated
// println!("{}", s1); // Error: moved
println!("{}", s2);
// Copy types (Copy on the stack):
let a = 5;
let b = a; // copy, both valid
println!("{} {}", a, b);
// Function arguments also move:
// fn take(s: String) {}
// Use return value to bring it back

References

&T is a shared reference that doesn't transfer ownership. The original is still usable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let s = String::from("hello");
// Immutable reference:
let len = calculate_length(&s);
println!("{}", len); // 5
println!("{}", s); // still usable
}
fn calculate_length(s: &String) -> usize {
s.len() // borrow without owning
}
// Reference rules:
// &T is a read-only borrow
// References do not transfer ownership
// References are immutable by default
// Multiple immutable references may coexist
// When a reference goes out of scope, the value is not dropped
// Common pattern:
// Use &str / &T for parameters to avoid copying

Mutable references

&mut T is a mutable reference. Only one mutable reference can exist at a time per scope.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // hello!
}
fn change(s: &mut String) {
s.push_str("!");
}
// Rules:
// Mutable reference &mut T
// Only one in the same scope
// Immutable and mutable cannot coexist:
// let r1 = &s;
// let r2 = &mut s; // Error
// Data races are forbidden at compile time:
// read-heavy, write-rare scenarios
// Can only reborrow after borrow ends
// Curly braces can shorten the borrow range

Dereference

* accesses the value behind a reference. Dereference pairs with reference.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let x = 5;
let r = &x; // reference
// Dereference to access the value:
println!("{}", *r);
// Dereference for assignment:
let mut y = 5;
let r2 = &mut y;
*r2 += 1; // modify y
println!("{}", y); // 6
// Auto-deref on method calls:
// s.len() needs no (*s).len()
// Indexing also dereferences under the hood
// Comparing references:
// does == compare values or references?
// It compares the dereferenced values
// Dangling references are rejected by the compiler

Lifetimes

'a annotates a reference's valid range. The borrow checker enforces validity. Elision rules.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Lifetime annotation:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long");
let s2 = String::from("s");
let r = longest(&s1, &s2);
println!("{}", r);
}
// Rules:
// 'a denotes the valid range of a reference
// The return lifetime is the intersection of the inputs
// Structs can hold references:
// struct X<'a> { part: &'a str }
// Elision rules:
// single input elided to the output
// methods elide the self lifetime
// In most cases, explicit annotation is not needed

Slices

&[T] is a view over contiguous elements. String slice is &str. Neither owns data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// Array slice:
let arr = [1, 2, 3, 4, 5];
let s: &[i32] = &arr[1..4]; // [2,3,4]
println!("{:?}", s);
// String slice:
let st = String::from("hello");
let part: &str = &st[0..2]; // "he"
// Panics if the boundary is not on a char boundary
// Full slice:
let all = &arr[..];
// Common patterns:
// Use &[T] rather than Vec for function parameters
// &str is a borrow of String
// slice length is unknown at compile time
// Passing a slice is a reference, not a copy

Deref coercion

The Deref trait makes &String coerce to &str, etc. Convenient argument polymorphism.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::ops::Deref;
fn main() {
let s = String::from("hello");
// &String automatically becomes &str:
let t: &str = &s;
// Auto-conversion at function parameter:
fn takes_str(s: &str) {}
let s2 = String::from("world");
takes_str(&s2); // auto deref
// Custom Deref:
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
// Method calls also auto-deref:
// my_box.len() walks through each Deref layer
// Coercion chain: &MyBox<T> -> &T

Copy types

Types with Copy are duplicated on assignment rather than moved. Integers, floats, bool, char, etc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
// Copy types: still usable after assignment
let a = 5;
let b = a; // copy
println!("{} {}", a, b); // both valid
// Copy types include:
// All integers/floats/bool/char
// Tuples (all elements Copy)
// Arrays
// &T references (are Copy)
// Non-Copy:
// String, Vec, Box
// Structs are non-Copy by default:
#[derive(Clone, Copy)]
struct Point { x: i32, y: i32 }
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy
println!("{} {}", p1.x, p2.x);
// Rule:
// Must have no Drop to be Copy
// Use .clone() for explicit cloning

5.Control Flow

if, match, and loops. Rust control flow is expression-based.

if expression

if/else is an expression that returns a value. No parens around the condition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let n = 7;
// Branches:
if n > 0 {
println!("positive");
} else if n == 0 {
println!("zero");
} else {
println!("negative");
}
// if is an expression:
let sign = if n > 0 { "+" } else { "-" };
println!("{}", sign);
// No parentheses around the condition
// Condition must be bool
// Branch return types must match
// An if without else returns ()

match

match exhausts patterns. Each arm is an expression. Patterns, ranges, and guards.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let x = 3;
// Match literals:
match x {
1 => println!("one"),
2 | 3 => println!("two or three"), // or
4..=6 => println!("four to six"), // range
_ => println!("other"), // wildcard
}
// Bindings and guards:
let num = Some(10);
match num {
Some(n) if n > 5 => println!("big {}", n),
Some(n) => println!("small {}", n),
None => println!("none"),
}
// Must be exhaustive (without _, compile error)
// match can return an expression value

while loop

while is a conditional loop. Repeats while the condition is true.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let mut n = 3;
// Counted loop:
while n > 0 {
println!("{}", n);
n -= 1;
}
// Conditional loop:
let mut line = String::new();
while line.trim() != "quit" {
line.clear();
// read input...
break; // example: break directly
}
// while loop has no index variable
// Prefer for to iterate a collection
// When an index is needed:
// a counter + while
// while returns ()

for loop

for iterates over ranges and iterators. Rust's preferred loop form.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
// Range:
for i in 0..5 {
println!("{}", i); // 0..4
}
// Inclusive range:
for i in 0..=5 {
println!("{}", i); // 0..5
}
// Iterate an array:
let arr = [10, 20, 30];
for v in &arr {
println!("{}", v);
}
// Need an index:
for (i, v) in arr.iter().enumerate() {
println!("{}: {}", i, v);
}
// Reverse:
for i in (0..3).rev() {}
// Iterating a collection doesn't transfer ownership
// Use into_iter for a moving iteration

Infinite loop

loop runs forever until break. Can return a value. Supports labels.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// Infinite loop:
let mut count = 0;
let result = loop {
count += 1;
if count == 3 {
break count * 10; // returns 30
}
};
println!("{}", result);
// Label to break out of the outer loop:
'outer: loop {
loop {
break 'outer; // break out of outer
}
}
// Uses:
// retries, polling, event loops
// Equivalent to while true
// loop is more idiomatic and can return a value

break & continue

break exits the loop. continue skips the current iteration. Both work with labels.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fn main() {
let mut n = 0;
loop {
n += 1;
// Skip even numbers:
if n % 2 == 0 {
continue;
}
if n > 7 {
break;
}
println!("{}", n); // 1 3 5 7
}
// continue skips the current iteration
// break with a value:
let v = loop { break 42; };
// Nested labels:
'a: for i in 0..3 {
for j in 0..3 {
if i + j == 2 {
continue 'a; // continue the outer loop
}
}
}
}

if let

if let simplifies matching for a single pattern. Use when only one variant matters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let opt = Some(5);
// Match a single pattern:
if let Some(v) = opt {
println!("value {}", v);
}
// With else:
if let Some(v) = opt {
println!("has value {}", v);
} else {
println!("no value");
}
// Other patterns:
let res: Result<i32, &str> = Ok(10);
if let Ok(v) = res {
println!("ok {}", v);
}
// Equivalent match:
// match opt {
// Some(v) => ...,
// _ => (),
// }
// More concise, focused on one case
// Non-exhaustive, composable

while let

while let loops while the pattern continues to match. Good for iterator-style draining.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let mut stack = vec![1, 2, 3];
// Pop until empty:
while let Some(top) = stack.pop() {
println!("{}", top); // 3 2 1
}
// Process a char iterator:
let mut chars = "abc".chars();
while let Some(c) = chars.next() {
println!("{}", c);
}
// Equivalent to loop + match:
// loop {
// match v.next() {
// Some(x) => ...
// None => break,
// }
// }
// For iterators and iterable objects
// Use loop for complex conditions

6.Functions

Function definitions, return values, ownership transfer, closures, and generics.

Function definition

fn defines a function. Parameters need type annotations. The body is an expression.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// No return value:
fn greet(name: &str) {
println!("hello {}", name);
}
// With return value:
fn add(a: i32, b: i32) -> i32 {
a + b // last expression
}
// Explicit return:
fn early(x: i32) -> i32 {
if x < 0 {
return 0;
}
x * 2
}
fn main() {
greet("Rust");
println!("{}", add(1, 2));
println!("{}", early(-5));
}
// Returning () can omit ->

Return value

The last expression is the return value. A semicolon turns it into a statement.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let x = five();
println!("{}", x); // 5
let y = with_semi();
println!("{:?}", y); // ()
}
fn five() -> i32 {
5 // expression, returns the value
}
fn with_semi() {
5; // statement, returns ()
}
// Key points:
// expression = code that has a value
// statement = ends with a semicolon
// no semicolon = the value is returned
// a block is an expression:
let n = { let a = 1; a + 2 }; // 3
// if/match/loop are also expressions

Parameters

Parameters are immutable by default. Destructured patterns. References avoid copying.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Reference parameter:
fn show(name: &str) {
println!("{}", name);
}
// Mutable parameter requires mut:
fn append(v: &mut Vec<i32>) {
v.push(1);
}
// Tuple destructuring parameter:
fn sum((a, b): (i32, i32)) -> i32 {
a + b
}
fn main() {
show("nick");
let mut v = vec![];
append(&mut v);
println!("{}", sum((1, 2)));
}
// Parameters are copies or moves of the value
// Pass by reference for large types
// Pass &mut when you need to modify
// Return a new value to avoid references

Ownership parameters

Pass-by-value moves ownership. Borrowing allows reuse. Return ownership when needed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn takes(s: String) {
println!("{}", s); // s is used here and then dropped
} // s goes out of scope and is dropped
fn main() {
let s = String::from("hi");
takes(s);
// println!("{}", s); // Error: moved
// Take ownership back:
let s2 = String::from("keep");
let s2 = give_back(s2);
println!("{}", s2);
}
fn give_back(s: String) -> String {
s // return moves it back
}
// Alternative:
// &str borrowing is usually better
// Only pass by value when you need ownership
// Return a tuple for multiple values

Borrow parameters

&T for read-only borrow, &mut T for mutable borrow. Both avoid moving ownership.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Read-only borrow:
fn len(s: &str) -> usize {
s.len()
}
// Multiple read-only borrows can coexist:
fn print_two(a: &str, b: &str) {
println!("{} {}", a, b);
}
// Mutable borrow:
fn push_item(v: &mut Vec<i32>, x: i32) {
v.push(x);
}
fn main() {
let s = String::from("abc");
println!("{}", len(&s));
println!("{}", s); // still usable after borrow
let mut v = vec![1];
push_item(&mut v, 2);
println!("{:?}", v);
}
// &str parameter is more general than &String

Closures

Closures capture environment values. |params| syntax. Can be stored or passed to functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// Definition:
let add = |a: i32, b: i32| a + b;
println!("{}", add(1, 2));
// Capture outer variables:
let factor = 3;
let mul = |x: i32| x * factor;
println!("{}", mul(5)); // 15
// A closure can mutate captures (needs mut):
let mut count = 0;
let mut inc = || {
count += 1;
count
};
println!("{}", inc()); // 1
// As an argument:
let evens: Vec<i32> =
(1..=6).filter(|x| x % 2 == 0).collect();
println!("{:?}", evens);
// move closures are covered in the Concurrency section

Function pointers

fn is a pointer to a regular function. Use the fn(T) -> U type. Closures can coerce to fn.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn add_one(x: i32) -> i32 {
x + 1
}
// Function pointer type:
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn main() {
let f: fn(i32) -> i32 = add_one;
println!("{}", f(10)); // 11
println!("{}", apply(add_one, 10));
// A closure that captures nothing can become an fn:
let c = |x: i32| x * 2;
let g: fn(i32) -> i32 = c;
println!("{}", g(5));
// Function in an array/vector:
let funcs = [add_one, add_one];
// Use the Fn trait for generic scenarios

Generic functions

Generic parameter <T> abstracts types. Optional bounds. Type inference.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Generic function:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut max = &list[0];
for item in list {
if item > max {
max = item;
}
}
max
}
fn main() {
let nums = vec![34, 50, 25];
println!("{}", largest(&nums));
let chars = vec!['y', 'm', 'a'];
println!("{}", largest(&chars));
}
// T: PartialOrd is a trait bound
// Multiple parameters: fn pair<T, U>
// Types inferred at the call site
// Explicit form: largest::<i32>(&nums)
// No runtime cost from generics (monomorphization)

7.Strings

&str and String, operations, formatting, parsing, and iteration.

&str & String

&str is a borrowed string slice, String is an owned growable string.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// &str string literal:
let s: &str = "hello";
// String owns the heap allocation:
let mut owned = String::from("hello");
owned.push_str(" world");
// &str -> String:
let a: String = s.to_string();
let b: String = String::from(s);
// String -> &str (borrow):
let c: &str = &owned;
let d: &str = owned.as_str();
// Differences:
// &str has a fixed length and does not own data
// String can grow and is heap-allocated
// Prefer &str for parameters
// Convert to String only when you need ownership

String operations

Push, insert, replace, remove. Methods that mutate a String.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
let mut s = String::from("hello");
// Append:
s.push_str(", world"); // &str
s.push('!'); // single char
// Insert:
s.insert(5, ' ');
// Replace:
let t = s.replace("hello", "hi");
// Remove:
s.pop(); // remove last char
s.truncate(5); // truncate
// Clear:
s.clear();
// Concatenate:
let s1 = String::from("a");
let s2 = String::from("b");
let s3 = s1 + &s2; // s1 is moved
let s4 = format!("{} {}", s3, "c");
// Length: s.len() returns bytes
}

Formatting

format! composes strings. Placeholders and format specifiers. Safe and efficient.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let name = "Nick";
let age = 30;
// Basic concatenation:
let s = format!("{} is {} years old", name, age);
println!("{}", s);
// Numbered/named:
format!("{0} {1} {0}", "a", "b");
format!("{name}-{age}", name = name, age = age);
// Alignment and width:
format!("{:>8}", "right"); // right-aligned
format!("{:<8}", "left"); // left-aligned
format!("{:^8}", "center");
// Number formatting:
format!("{:.2}", 3.14159); // 3.14
format!("{:+}", 42); // +42
format!("{:08b}", 10); // binary, zero-padded
format!("{:#x}", 255); // 0xff
// Debug format:
format!("{:?}", vec![1, 2]);

String slicing

&s[a..b] slices by bytes. Must fall on character boundaries, otherwise panic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let s = String::from("hello 世界");
// Byte slice:
let part = &s[0..5]; // "hello"
println!("{}", part);
// Char boundary:
// each char in "世界" is 3 bytes
let w = &s[6..12]; // "世界"
// Bad boundary panics:
// let bad = &s[0..6]; // not on a char boundary
// Safe iteration:
for c in s.chars() {
println!("{}", c);
}
// Get a single char:
let first = s.chars().next();
// By line:
for line in s.lines() {}
// Indexing is by byte, not by char

String iteration

chars() iterates characters, bytes() iterates bytes, char_indices yields positions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
let s = "你好Rust";
// Iterate by char:
for c in s.chars() {
println!("{}", c);
}
// Iterate by byte:
for b in s.bytes() {
println!("{:02x}", b);
}
// Char + byte offset:
for (i, c) in s.char_indices() {
println!("{} at {}", c, i);
}
// By line:
for line in "a\nb\n".lines() {
println!("<{}", line);
}
// Split:
for part in "a,b,c".split(",") {
println!("{}", part);
}
// Count chars:
let n = s.chars().count();
println!("{} chars", n);
// Byte count len() is not the same as char count

String parsing

str::parse converts a string into a value. parse::<T>() needs an explicit target type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
// Parse to i32:
let n: i32 = "42".parse().unwrap();
// Explicit turbofish:
let f: f64 = "3.14".parse().unwrap();
let u: u32 = "255".parse().unwrap();
// Error handling:
let result = "abc".parse::<i32>();
match result {
Ok(v) => println!("{}", v),
Err(e) => println!("parse failed: {}", e),
}
// Bool:
let b: bool = "true".parse().unwrap();
// Number to string:
let s = 42.to_string();
let s2 = format!("{}", 3.14);
// ? shorthand:
// fn f() -> Result<i32, ParseIntError> {
// Ok("42".parse()?)
// }

Character operations

char methods: case conversion, digit and whitespace checks. Chained with string methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
let c = 'a';
// Checks:
println!("{}", c.is_alphabetic());
println!("{}", c.is_numeric());
println!("{}", c.is_whitespace());
println!("{}", c.is_ascii());
// Case conversion:
let up = 'a'.to_uppercase().to_string();
let low = 'A'.to_lowercase().to_string();
// String methods:
let s = "Hello, Rust!";
s.to_lowercase();
s.to_uppercase();
s.trim(); // trim whitespace
s.starts_with("Hello");
s.contains("Rust");
s.replace("Rust", "Go");
// Substring:
let sub = &s[7..]; // "Rust!"
// Empty check: s.is_empty()
// Repeat: "ab".repeat(3)

String methods

Search, trim, split, predicate methods. Common str methods are chainable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let s = " Hello, Rust 2024 ";
// Trim and case:
let t = s.trim();
let lower = t.to_lowercase();
let upper = t.to_uppercase();
// Queries:
t.starts_with("Hello");
t.ends_with("2024");
t.contains("Rust");
t.find("Rust"); // Option<usize>
// Split:
let parts: Vec<&str> = t.split(",").collect();
let lines: Vec<&str> = t.lines().collect();
let words: Vec<&str> = t.split_whitespace().collect();
// Replace and remove:
t.replace("Rust", "Go");
t.replacen("o", "0", 1);
// Repeat and pad:
"ab".repeat(3);
format!("{:0>5}", 42); // 00042
// Empty check:
s.is_empty();
}

8.Collections

Vec, HashMap, set types, and iterators.

Vec

Vec<T> is a growable array. Push and remove at will. Pointer on the stack, data on the heap.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
// Create:
let mut v = Vec::new();
v.push(1);
v.push(2);
// Macro create:
let mut v2 = vec![10, 20, 30];
let zero = vec![0; 5]; // [0,0,0,0,0]
// Access:
let a = &v2[0]; // index, panics out of bounds
let b = v2.get(1); // Option, safe
// Modify:
v2[0] = 99;
v2.push(40);
v2.pop(); // remove last
v2.remove(1); // remove by index
v2.insert(0, 5); // insert at the front
// Length: v2.len()
// Clear: v2.clear()
// Iterate: for x in &v2 {}
// Sort: v2.sort()

Vec operations

Iterate, filter, map, collect. Slice views and ownership of the underlying Vec.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let v = vec![1, 2, 3, 4];
// Iterator transforms:
let doubled: Vec<i32> =
v.iter().map(|x| x * 2).collect();
// Filter:
let evens: Vec<&i32> =
v.iter().filter(|x| **x % 2 == 0).collect();
// Sum:
let sum: i32 = v.iter().sum();
// Max:
let max = v.iter().max();
// Find:
let found = v.iter().find(|x| **x == 3);
// Iterate to mutate:
let mut w = vec![1, 2];
for x in w.iter_mut() {
*x += 10;
}
// Move iteration:
let owned: Vec<i32> = w.into_iter()
.map(|x| x + 1).collect();
// Slice view:
let part = &v[1..3];

HashMap

HashMap<K, V> stores key/value pairs. Hash-backed and unordered. O(1) lookups.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::collections::HashMap;
fn main() {
// Create:
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Red"), 20);
// Macro create:
let mut m: HashMap<&str, i32> =
HashMap::from([("a", 1), ("b", 2)]);
// Read:
let score = scores.get("Blue"); // Option
// Iterate:
for (k, v) in &scores {
println!("{}: {}", k, v);
}
// Insert or update:
*m.entry("a").or_insert(0) += 10;
m.insert("c", 3);
// Remove:
m.remove("a");
// Check:
m.contains_key("b");
// Length: m.len()
// Unordered, iteration order is arbitrary

HashSet

HashSet<T> holds unique elements. Dedupe, intersection, union, difference.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::collections::HashSet;
fn main() {
// Create:
let mut set = HashSet::new();
set.insert(1);
set.insert(2);
set.insert(2); // duplicate ignored
println!("{:?}", set); // {1, 2}
// Operations:
set.contains(&1); // true
set.remove(&1);
set.len();
// Set operations:
let a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [2, 3, 4].into_iter().collect();
// Union:
let u: HashSet<_> = a.union(&b).cloned().collect();
// Intersection:
let i: HashSet<_> = a.intersection(&b).cloned().collect();
// Difference:
let d: HashSet<_> = a.difference(&b).cloned().collect();
// Subset check: a.is_subset(&b)

BTree

BTreeMap / BTreeSet keep keys sorted. Iterate in order. Slightly slower than hash.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::collections::BTreeMap;
fn main() {
// Ordered key-value map:
let mut m = BTreeMap::new();
m.insert("b", 2);
m.insert("a", 1);
m.insert("c", 3);
// Iterate in order:
for (k, v) in &m {
println!("{}: {}", k, v);
} // a b c in order
// Range query:
for (_k, v) in m.range("a".."c") {
println!("{}", v);
}
// First/last:
let first = m.first_key_value();
let last = m.last_key_value();
// Same idea for BTreeSet:
use std::collections::BTreeSet;
let s: BTreeSet<i32> = [3, 1, 2].into_iter().collect();
println!("{:?}", s); // {1, 2, 3}
// Use when:
// you need ordered iteration / range queries
// keys are comparable

Iterators

The Iterator trait drives chained adaptors. Lazy evaluation. Consumed methods execute.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
let v = vec![1, 2, 3, 4, 5];
// Chaining:
let result: Vec<i32> = v.iter()
.filter(|x| **x % 2 == 1) // odd numbers
.map(|x| x * 10)
.collect();
println!("{:?}", result); // [10, 30, 50]
// Lazy: nothing runs until consumed
// Common adapters:
// .take(n) take first n
// .skip(n) skip n
// .rev() reverse
// .zip(iter) pair up
// .enumerate() add index
// Consumers:
let sum: i32 = v.iter().sum();
let count = v.iter().count();
let any = v.iter().any(|x| x > 3);
let all = v.iter().all(|x| x > 0);
// Array to iterator:
for x in v.iter() { println!("{}", x); }

VecDeque

VecDeque is a double-ended queue. Efficient push/pop on both ends. Ring buffer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::collections::VecDeque;
fn main() {
// Create:
let mut d = VecDeque::new();
// Operations at both ends:
d.push_back(1);
d.push_back(2);
d.push_front(0);
println!("{:?}", d); // [0, 1, 2]
d.pop_front(); // 0
d.pop_back(); // 2
// Access:
d.front(); // Option
d.back();
d.front_mut();
// Range:
d.make_contiguous();
// Use cases:
// queue / FIFO
// sliding windows
// two-end scanning
// Difference from Vec:
// Vec front insertion is O(n)
// VecDeque is O(1) at both ends
// slightly slower random indexing

Nested collections

Combinations of collections. Vec<HashMap>, nested HashMap, multi-dim Vec.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::collections::HashMap;
fn main() {
// Vec of structs:
let users = vec![
("a", 1),
("b", 2),
];
// Nested HashMap:
let mut matrix: HashMap<String, Vec<i32>> =
HashMap::new();
matrix.insert("row1".to_string(), vec![1, 2]);
// Insert if missing:
matrix.entry(String::from("row2"))
.or_insert(vec![]).push(3);
// Multi-dimensional Vec:
let mut grid = vec![vec![0; 3]; 3];
grid[0][1] = 5;
// Collections of structs:
#[derive(Debug)]
struct Item { id: u32, tags: Vec<String> }
let items = vec![
Item { id: 1, tags: vec!["x".into()] },
];
println!("{:?}", items);
}

9.Memory & Ownership Extensions

Heap allocation, smart pointers, and shared ownership.

Stack & heap

Stack: fixed size, fast. Heap: dynamic size, allocated. Ownership governs lifetimes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
// Stack data:
let x = 42; // fixed-size, on the stack
// Heap data:
let s = String::from("dynamic size");
// String struct on the stack, data on the heap
// Automatic management:
// stack pops immediately when scope ends
// heap memory is released by the ownership system
// no manual free, no leaks
// Release timing:
// when owner leaves scope (Drop)
// Box / smart pointers control heap objects
// recursive structures need Box
// put large arrays on the heap to avoid stack overflow

Box

Box<T> puts a value on the heap. Fixed size, moveable. Required for recursive types.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
// Heap allocation:
let b = Box::new(5);
println!("{}", *b); // dereference
// Recursive types require Box:
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1,
Box::new(List::Cons(2, Box::new(List::Nil))));
// Box provides only single ownership
// Uses:
// large structs to reduce copies
// dynamically-sized trait objects
// recursive data structures
// Auto-deref:
// b.len() derefs automatically
// auto-dropped when out of scope

Rc

Rc<T> is reference-counted shared ownership. Single-threaded. Read-only sharing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::rc::Rc;
fn main() {
// Shared ownership:
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // refcount +1
let c = Rc::clone(&a);
println!("{}", a);
println!("{}", b);
// Count:
println!("{}", Rc::strong_count(&a)); // 3
// Read-only sharing:
// data in Rc is immutable
// use RefCell for mutability
// Single-thread only:
// use Arc for multi-threaded
// Cycles cause leaks:
// break them with Weak
// refcount decreases as scopes end

RefCell

RefCell<T> enforces borrow rules at runtime. Interior mutability. Panics on conflict.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::cell::RefCell;
fn main() {
// Interior mutability:
let cell = RefCell::new(42);
*cell.borrow_mut() += 1; // mutate
println!("{}", cell.borrow());
// Runtime borrow rules:
// one mutable borrow, or
// multiple immutable borrows
// Conflict:
// let b1 = cell.borrow();
// let b2 = cell.borrow_mut(); // panic!
// Uses:
// mutable fields in structs
// shared mutability with Rc
// Difference from &mut:
// compile-time -> runtime
// bypasses the borrow checker
// be careful, keep borrows short

Arc

Arc<T> is thread-safe reference counting. Shared ownership across threads. Atomic counter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::sync::Arc;
use std::thread;
fn main() {
// Thread-safe sharing:
let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for i in 0..3 {
let d = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("thread {}: {:?}", i, d);
}));
}
for h in handles {
h.join().unwrap();
}
// Difference from Rc:
// Arc uses atomics, thread-safe
// Rc is single-threaded
// Read-only sharing:
// for mutability pair with Mutex:
// Arc<Mutex<T>>
// Watch out for reference cycles

Mutex

Mutex<T> provides exclusive access. The guard protects the inner value. Pair with Arc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Shared mutable state:
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1; // lock and mutate
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", *counter.lock().unwrap());
// Lock lifetime:
// lock() returns a Guard
// auto-unlocked when out of scope
// Poisoning:
// panic while holding the lock poisons it
// use unwrap or handle via or_else
// Mind lock order to avoid deadlocks

Atomic types

AtomicU32 and friends. Lock-free concurrent counters. Ordering memory orders.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use std::sync::atomic::{
AtomicU32, Ordering
};
use std::thread;
fn main() {
// Atomic counter:
let counter = AtomicU32::new(0);
let mut handles = vec![];
for _ in 0..10 {
let c = &counter;
handles.push(thread::spawn(move || {
c.fetch_add(1, Ordering::SeqCst);
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", counter.load(Ordering::SeqCst));
// Types:
// AtomicU8..AtomicI64
// AtomicBool AtomicUsize
// Operations:
// fetch_add / fetch_sub
// compare_exchange
// load / store
// Memory orderings:
// Relaxed / Release / Acquire
// SeqCst is the strictest
// Lock-free, non-blocking

Smart pointers

Deref / Drop traits. Implemented by Box / Rc / Arc. Pointer abstraction with cleanup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use std::ops::Deref;
// Custom smart pointer:
struct MyBox<T>(T);
// Deref allows dereferencing:
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
// Drop handles cleanup:
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
println!("dropped");
}
}
fn main() {
let m = MyBox(5);
println!("{}", *m); // dereference to 5
// auto-dropped when out of scope
// Standard smart pointers:
// Box<T> heap allocation
// Rc<T> single-threaded sharing
// Arc<T> multi-threaded sharing
// RefCell / Mutex for interior mutability
// String / Vec are also pointer + capacity

10.Traits & Generics

Trait abstraction, implementations, generic bounds, and polymorphism.

trait definition

A trait defines shared behavior. Method signatures. Like an interface.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Define a trait:
#[derive(Debug)]
struct Dog;
struct Cat;
// Behaviour abstraction:
trait Speak {
fn sound(&self) -> &'static str;
}
// Implement for types:
impl Speak for Dog {
fn sound(&self) -> &'static str {
"woof"
}
}
impl Speak for Cat {
fn sound(&self) -> &'static str {
"meow"
}
}
fn main() {
let dog = Dog;
let cat = Cat;
println!("{}", dog.sound());
println!("{}", cat.sound());
}
// Traits may provide default methods
// implementations must satisfy the method signatures
// a type may implement multiple traits

Implementing a trait

impl Trait for Type implements the trait. Orphan rule: either the trait or type must be local.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Implement a custom trait for built-in types:
trait Greeting {
fn hello(&self) -> String;
}
// For &str:
impl Greeting for &str {
fn hello(&self) -> String {
format!("hello, {}", self)
}
}
// For Vec:
impl Greeting for Vec<i32> {
fn hello(&self) -> String {
format!("{:?} length {}", self, self.len())
}
}
fn main() {
println!("{}", "Nick".hello());
println!("{}", vec![1, 2].hello());
}
// Orphan rule:
// at least one of trait or type must be local
// otherwise compile error
// Empty impls:
// marker (sealed) types
// generic impls can be reused

Default methods

Trait methods can have default implementations. Override is optional. Calls are dispatched.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
trait Animal {
fn name(&self) -> String;
// Default impl:
fn intro(&self) -> String {
format!("I am {}", self.name())
}
}
struct Dog;
impl Animal for Dog {
fn name(&self) -> String {
"puppy".to_string()
}
// Optionally override:
fn intro(&self) -> String {
format!("{} woof", self.name())
}
}
struct Cat;
impl Animal for Cat {
fn name(&self) -> String {
"kitten".to_string()
}
// Uses default intro
}
fn main() {
println!("{}", Dog.intro()); // puppy woof
println!("{}", Cat.intro()); // I am kitten
}
// Default methods can call other trait methods

trait objects

dyn Trait is dynamic dispatch. Heterogeneous collections. Runtime polymorphism.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
trait Draw {
fn draw(&self);
}
struct Circle;
struct Square;
impl Draw for Circle {
fn draw(&self) {
println!("draw circle");
}
}
impl Draw for Square {
fn draw(&self) {
println!("draw square");
}
}
// Dynamic dispatch:
fn draw_all(shapes: &[&dyn Draw]) {
for s in shapes {
s.draw();
}
}
// Boxed trait object:
fn make() -> Box<dyn Draw> {
Box::new(Circle)
}
fn main() {
let shapes: Vec<&dyn Draw> =
vec![&Circle, &Square];
draw_all(&shapes);
// Which impl runs is decided at runtime
// dyn has runtime overhead
// generics are faster with static dispatch
// object-safety constraints

Generics

Generic types and functions. T abstracts. Code reuse. Zero-cost via monomorphization.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Generic function:
fn identity<T>(x: T) -> T {
x
}
// Generic struct:
struct Point<T> {
x: T,
y: T,
}
// Multiple type parameters:
struct Pair<A, B> {
first: A,
second: B,
}
// Generic method:
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 1, y: 2 }; // inferred i32
println!("{}", p.x());
let ip = Point { x: 1.5, y: 2.5 };
println!("{}", ip.x());
// Monomorphization:
// compiler generates concrete versions
// no runtime overhead
// more efficient than dyn

Trait bounds

T: Trait bounds. where clauses. Combine bounds with +.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Basic bounds:
fn print_it<T: std::fmt::Debug>(x: &T) {
println!("{:?}", x);
}
// where clause:
fn complex<T, U>(a: &T, b: &U)
where
T: std::fmt::Display + Clone,
U: std::fmt::Debug,
{
println!("{}", a);
println!("{:?}", b);
}
// Generic return:
fn pair() -> impl std::fmt::Display {
42 // opaque return type
}
// Bounded generic collections:
fn sortable<T: Ord>(list: &mut [T]) {
list.sort();
}
// Bounds make generic methods available
// impl Trait shorthand
// multiple bounds combined with +

impl blocks

impl blocks define associated functions and methods. self / Self. Constructor conventions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Associated function (no self):
fn new(w: u32, h: u32) -> Rectangle {
Rectangle { width: w, height: h }
}
// Method (&self):
fn area(&self) -> u32 {
self.width * self.height
}
// Mutable method (&mut self):
fn set_width(&mut self, w: u32) {
self.width = w;
}
// Consume self:
fn into_string(self) -> String {
format!("{}x{}", self.width, self.height)
}
}
fn main() {
let mut r = Rectangle::new(2, 3);
println!("{}", r.area());
r.set_width(5);
// Self refers to the current type inside impl
// multiple impl blocks can be split across the file

Composition & polymorphism

Rust has no inheritance. Use trait + composition. Default methods play the base-class role.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Composition over inheritance:
struct Engine {
power: u32,
}
impl Engine {
fn start(&self) {
println!("engine starts {} hp", self.power);
}
}
struct Car {
engine: Engine, // composition
model: String,
}
impl Car {
fn start(&self) {
self.engine.start(); // delegation
println!("{} drives off", self.model);
}
}
// Default trait methods emulate base classes:
trait Fly {
fn fly(&self) {
println!("flying");
}
}
struct Bird;
impl Fly for Bird {} // uses default
// Polymorphism:
fn make_fly(f: &dyn Fly) {
f.fly();
}
fn main() {
let car = Car {
engine: Engine { power: 150 },
model: String::from("Guru"),
};
car.start();
}
// Composition is more flexible and avoids the diamond problem

11.Error Handling

Result, Option, the ? operator, panic, and custom errors.

Result

Result<T, E> returns success or error. Ok / Err variants.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::fs::File;
fn main() {
// Result enum:
let result = File::open("nope.txt");
// match handling:
match result {
Ok(file) => println!("open succeeded"),
Err(e) => println!("open failed: {}", e),
}
// Generic signature:
// enum Result<T, E> {
// Ok(T),
// Err(E),
// }
// Construct manually:
let ok: Result<i32, String> = Ok(42);
let err: Result<i32, String> =
Err(String::from("failed"));
// Methods:
let v = ok.unwrap_or(0);
let v2 = ok.unwrap_or_else(|_| -1);
}

unwrap & expect

unwrap returns the value or panics. expect adds a custom panic message. For debugging.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let ok: Result<i32, &str> = Ok(10);
// unwrap:
let v = ok.unwrap();
println!("{}", v);
// Panic on failure:
// let e: Result<i32, &str> = Err("bad");
// e.unwrap(); // panic!
// expect with message:
// let e = Err("bad");
// e.expect("should succeed"); // panic with that message
// Suitable for:
// prototypes / examples / tests
// logically cannot fail
// In production:
// propagate with ?
// or match / handle
// unwrap_or to provide a default

? operator

? short-circuits error propagation. Err returns early, Ok unwraps and continues.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::fs::File;
use std::io::Read;
// Propagate errors:
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut f = File::open(path)?; // Err returned directly
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
// Chaining:
fn first_line(path: &str) -> Result<String, std::io::Error> {
let content = read_file(path)?;
Ok(content.lines().next().unwrap_or("").to_string())
}
// ? auto-converts errors:
// requires a From impl
// fn f() -> Result<(), Box<dyn Error>> {
// File::open("x")?; // io::Error boxed automatically
// Ok(())
// }
// Function must return Result / Option

Implementing Error

The standard Error trait. Display + Debug. Error source chain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::error::Error;
use std::fmt;
// Custom error type:
#[derive(Debug)]
struct MyError {
msg: String,
}
// Display:
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "an error occurred: {}", self.msg)
}
}
// Implement Error:
impl Error for MyError {}
fn main() {
let e = MyError { msg: "xxx".to_string() };
println!("{}", e); // Display
println!("{:?}", e); // Debug
println!("{}", e.source()); // no source
}
// Error trait requires Display + Debug
// source() returns the underlying error
// used by ? and error chains

Custom errors

Design custom error types. Enum variants carry context. Classify errors by category.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#[derive(Debug)]
// Categorise errors:
enum AppError {
NotFound(String),
InvalidInput { field: String, reason: String },
Io(std::io::Error),
}
// Implement Display manually:
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::NotFound(msg) =>
write!(f, "not found: {}", msg),
AppError::InvalidInput { field, reason } =>
write!(f, "{} invalid: {}", field, reason),
AppError::Io(e) => write!(f, "IO: {}", e),
}
}
}
impl std::error::Error for AppError {}
// Conversion:
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> AppError {
AppError::Io(e)
}
}
// Use:
// fn f() -> Result<(), AppError> {
// std::fs::read_to_string("x")?; // auto-converts
// Ok(())
// }

thiserror

The thiserror derive macro auto-implements Display / Error. Greatly reduces boilerplate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Add to Cargo.toml:
// thiserror = "2"
use thiserror::Error;
// Derive macro:
#[derive(Error, Debug)]
// Error enum:
#[derive(Error, Debug)]
enum DataError {
#[error("record not found: {0}")]
NotFound(String),
#[error("validation failed: {field} {reason}")]
Validation {
field: String,
reason: String,
},
#[error(transparent)]
Io(#[from] std::io::Error),
}
// Automatically provides:
// Display impl
// Error impl
// From<std::io::Error> conversion
fn main() {
let e = DataError::NotFound("id".into());
println!("{}", e);
}
// Cuts down boilerplate dramatically
// ecosystem-standard approach

Option

Option<T> for possibly-missing values. Some / None. Safe access with combinators.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
// Option type:
let some = Some(5);
let none: Option<i32> = None;
// match:
match some {
Some(v) => println!("value {}", v),
None => println!("no value"),
}
// Common methods:
let x = some.unwrap_or(0);
let y = some.unwrap_or_else(|| compute());
let z = none.unwrap_or(0); // 0
// Chaining:
let doubled = some.map(|v| v * 2);
let filtered = some.filter(|v| v > 3);
let flat = Some(Some(1)).flatten();
// Combining:
let a = Some(1);
let b = Some(2);
let sum = a.zip(b).map(|(x, y)| x + y);
// Common cases:
// Vec::get / HashMap::get
// propagate None with ?

panic

panic! for unrecoverable errors. Crashes and unwinds. Only for bugs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
// Trigger panic:
// panic!("fatal error");
// Out-of-bounds index:
let v = vec![1, 2, 3];
// let x = v[10]; // panic!
// Empty slice first item:
let empty: &[i32] = &[];
// let x = empty[0]; // panic!
// Panic behaviour:
// prints the error message
// unwinds the stack, running Drop
// terminates the program
// Custom message:
// panic!("index {} out of bounds", 10);
// Use when:
// bug the program cannot recover from
// a contract is violated
// Use Result for recoverable errors

12.Input / Output

Standard input/output, file reading/writing, and the file system.

Reading input

stdin read_line into a String. trim removes the trailing newline.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::io;
fn main() {
// Read a line:
let mut input = String::new();
io::stdin().read_line(&mut input)
.expect("read failed");
// Trim newline:
let trimmed = input.trim();
println!("you entered: {}", trimmed);
// Read a number:
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
let n: i32 = line.trim().parse().unwrap();
println!("number: {}", n);
// Read in a loop:
loop {
let mut s = String::new();
let bytes = io::stdin().read_line(&mut s).unwrap();
if bytes == 0 { break; } // EOF
print!("{}", s);
}
}

Reading files

read_to_string slurps the whole file. Read trait streams bytes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::fs;
fn main() {
// One-shot read as string:
let content = fs::read_to_string("data.txt")
.expect("read failed");
println!("{}", content);
// Read as bytes:
let bytes = fs::read("data.bin").unwrap();
// Streaming read (large file):
use std::io::{Read, BufReader};
let f = fs::File::open("big.log").unwrap();
let mut reader = BufReader::new(f);
let mut buf = String::new();
reader.read_to_string(&mut buf).unwrap();
// Read line by line:
use std::io::BufRead;
let f = fs::File::open("data.txt").unwrap();
for line in std::io::BufReader::new(f).lines() {
println!("{}", line.unwrap());
}
}

Writing files

write / append to a file. OpenOptions controls mode. Buffered writing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::fs;
fn main() {
// Overwrite write:
fs::write("out.txt", "hello\n")
.expect("write failed");
// Append write:
use std::fs::OpenOptions;
use std::io::Write;
let mut f = OpenOptions::new()
.append(true)
.create(true)
.open("log.txt")
.unwrap();
f.write_all(b"more data\n").unwrap();
// Buffered write:
let mut writer = std::io::BufWriter::new(
fs::File::create("buf.txt").unwrap());
writeln!(writer, "line {}", 1).unwrap();
writer.flush().unwrap(); // flush to disk
// Formatted write:
// write!(f, "{}", value)

File system

Create directories, delete, rename, test existence. Path operations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::fs;
fn main() {
// Create directories:
fs::create_dir("data").unwrap();
fs::create_dir_all("data/a/b").unwrap();
// Remove:
fs::remove_file("tmp.txt").unwrap();
fs::remove_dir("data/a").unwrap();
fs::remove_dir_all("data").unwrap();
// Rename:
fs::rename("a.txt", "b.txt").unwrap();
// Existence check:
if fs::metadata("b.txt").is_ok() {
println!("exists");
}
// Metadata:
let meta = fs::metadata("b.txt").unwrap();
println!("size: {}", meta.len());
println!("is dir: {}", meta.is_dir());
// Copy:
fs::copy("b.txt", "c.txt").unwrap();
}

BufRead line by line

BufRead for line-by-line reads. lines(), read_line, split.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::io::BufRead;
use std::io::BufReader;
use std::fs::File;
fn main() {
let f = File::open("data.txt").unwrap();
let reader = BufReader::new(f);
// Line by line:
for line in reader.lines() {
let l = line.unwrap();
println!("{}", l);
}
// With line numbers:
let f = File::open("data.txt").unwrap();
for (i, line) in
BufReader::new(f).lines().enumerate() {
println!("{}: {}", i + 1, line.unwrap());
}
// Manually read one line:
// let mut buf = String::new();
// reader.read_line(&mut buf)
// Split on byte:
// for part in reader.split(b",") {}
// Also works with stdin

Output & writing

print! / println! / eprintln!. write! / writeln! to any writer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::io::{self, Write};
fn main() {
// stdout:
println!("normal output");
print!("no newline");
// stderr:
eprintln!("error message");
// Write to Vec:
let mut buf = Vec::new();
writeln!(buf, "write to Vec {}", 1).unwrap();
println!("{:?}", buf);
// Write to String:
let mut s = String::new();
write!(s, "formatted {}", 42).unwrap();
println!("{}", s);
// Write to file (see file section)
// io::stdout() also works:
// let mut out = io::stdout();
// writeln!(out, "write directly to stdout")
// Flush manually

Standard streams

stdin / stdout / stderr. Interactive and piped data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::io::{self, Read, Write};
fn main() {
// Read everything from a pipe:
// echo data | program
let mut buf = String::new();
io::stdin().read_to_string(&mut buf).unwrap();
// Process piped input line by line:
// for line in io::stdin().lines() {
// println!("{} ", line.unwrap());
// }
// Output to stdout:
println!("done");
// Errors to stderr:
eprintln!("warning: non-fatal");
// Write to stdout:
// io::stdout().write_all(b"raw\n")
// Interactive prompt:
// print!("name: ");
// io::stdout().flush().unwrap();
}

Byte I/O

read into a byte buffer. Stream binary data. write_all writes everything.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::fs::File;
use std::io::{self, Read, Write};
fn main() {
// Read into a buffer:
let mut f = File::open("data.bin").unwrap();
let mut buf = [0u8; 1024];
let n = f.read(&mut buf).unwrap();
println!("read {} bytes", n);
// Loop until EOF:
let mut f = File::open("data.bin").unwrap();
let mut total = 0;
let mut buf = [0u8; 256];
loop {
let n = f.read(&mut buf).unwrap();
if n == 0 { break; } // EOF
total += n;
// process buf[..n]
}
// Write all:
let mut out = File::create("out.bin").unwrap();
out.write_all(&[1, 2, 3, 4]).unwrap();
// Read whole file into bytes:
let all = std::fs::read("data.bin").unwrap();
println!("total {} bytes", all.len());
}

13.Common Pitfalls

The most common pitfalls for Rust beginners and the correct way to write them.

Use after move

After a move the original binding is unusable. Borrow or clone to keep ownership.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// BAD: access after move
fn main() {
let s = String::from("hi");
let t = s; // moved
// println!("{}", s); // Error: moved
println!("{}", t);
}
// GOOD: borrow
fn main() {
let s = String::from("hi");
let t = &s; // borrow
println!("{} {}", s, t);
}
// GOOD: explicit clone
let s = String::from("hi");
let t = s.clone();
// Scenarios:
// pass by value to transfer ownership
// need to use it in two places
// borrowing is more efficient, cloning is more direct

Borrow conflict

Shared and mutable borrows cannot coexist. Use scope to shorten borrows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// BAD: borrow at the same time
fn main() {
let mut v = vec![1, 2, 3];
// let r1 = &v; // immutable
// v.push(4); // mutable, conflict
// println!("{:?}", r1);
}
// GOOD: end the immutable borrow first
fn main() {
let mut v = vec![1, 2, 3];
let r1 = &v[0];
println!("{}", r1); // borrow ends
v.push(4); // now allowed
}
// GOOD: separate scopes
{
let r = &v;
println!("{:?}", r);
} // borrow ends
v.push(4);
// Rules:
// many readers or one writer
// the compiler guarantees no data races

Dangling reference

Returning a reference to a local is dangling. Return an owned value or a proper lifetime.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// BAD: return a reference to a local
// fn bad() -> &String {
// let s = String::from("x");
// &s // Error: s is dropped
// }
// GOOD: return ownership
fn good() -> String {
String::from("x")
}
// GOOD: reference from an input
fn first_char(s: &str) -> &str {
&s[..1]
}
// GOOD: lifetime parameter
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Rules:
// the returned reference must remain valid
// point to the heap or to a parameter's lifetime
// the compiler (borrow checker) rejects dangling references

String concatenation

+ moves its LHS. Repeated + is slow. Prefer format! or push_str.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// BAD: repeated + concatenation
let mut s = String::new();
// for _ in 0..10 {
// s = s + "x"; // reallocates every time
// }
// GOOD: push_str to append
let mut s = String::new();
for _ in 0..10 {
s.push_str("x");
}
// GOOD: format! once
let a = "hello";
let b = "world";
let s = format!("{}, {}", a, b);
// + syntax:
let s1 = String::from("a");
let s2 = String::from("b");
let s3 = s1 + &s2; // s1 is moved
// Reason:
// + requires a String LHS
// many concatenations cause frequent reallocation
// format! / join are better

Index type

Indices and lengths are usize, not i32. Type mismatch when indexing collections.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// BAD: i32 index
fn main() {
let v = vec![1, 2, 3];
let i: i32 = 1;
// let x = v[i]; // Error: usize required
// let x = v[i as usize]; // cast
}
// GOOD: usize directly
fn main() {
let v = vec![1, 2, 3];
let i: usize = 1;
let x = v[i]; // ok
println!("{}", x);
}
// Scenarios:
// for (i, x) in v.iter().enumerate()
// returns usize
// len() is also usize
// When types differ during distance arithmetic:
// cast with as usize
// or adjust the variable types

match arm types

All match arms must return the same type. Missing variants need a _ wildcard.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// BAD: mismatched branch types
// let x = match 5 {
// 1 => "one",
// _ => 42, // Error: str vs i32
// };
// GOOD: unified return type
let x = match 5 {
1 => "one",
_ => "other", // both are &str
};
// BAD: non-exhaustive
// enum Color { R, G, B }
// let c = Color::R;
// match c {
// Color::R => {}
// Color::G => {}
// // missing B, compile error
// }
// GOOD: add a wildcard
// match c {
// Color::R => {}
// _ => {}
// }
// Rules:
// exhaustiveness is checked at compile time
// types inferred from the first branch

Closure capture

Closures borrow or move automatically as needed. Conflicts arise. move explicit-transfers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// BAD: closure borrow vs mutation conflict
fn main() {
let mut count = 0;
// let print = || println!("{}", count);
// count += 1; // conflict: already borrowed
// print();
}
// GOOD: end the closure first
fn main() {
let mut count = 0;
{
let print = || println!("{}", count);
print();
} // borrow ends
count += 1;
}
// GOOD: move closure
let data = vec![1, 2];
let handler = move || {
println!("{:?}", data); // owns the data
};
// Rules:
// Fn / FnMut / FnOnce
// auto-selected by capture mode
// pass closure to a thread with move
// narrow the borrow scope when conflicting

Integer overflow

Debug builds panic on overflow. Release builds wrap. Handle explicitly or choose a wider type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// BAD: debug build overflow panics
// fn main() {
// let x: u8 = 255;
// let y = x + 1; // panics in debug!
// }
// GOOD: explicit check
fn main() {
let x: u8 = 255;
match x.checked_add(1) {
Some(v) => println!("{}", v),
None => println!("overflow"),
}
// Other methods:
// x.checked_add / checked_mul
// x.wrapping_add // wrap around
// x.saturating_add // saturate at max
// x.overflowing_add // returns carry
// release builds wrap by default
// use u64/usize for large counts
// pick the method whose semantics match

14.Concurrency

Threads, channels, shared state, and Send/Sync.

Threads

thread::spawn launches a thread. join waits and returns the result.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::thread;
use std::time::Duration;
fn main() {
// start a thread:
let handle = thread::spawn(|| {
for i in 1..3 {
println!("child thread {}", i);
thread::sleep(Duration::from_millis(10));
}
});
// main thread continues:
for i in 1..3 {
println!("main thread {}", i);
}
// wait for the child to finish:
handle.join().unwrap();
// join can return the closure's value:
let h = thread::spawn(|| 42);
let result = h.join().unwrap();
println!("{}", result); // 42
}

move closures

move closures transfer captures into the thread. Avoids dangling borrows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::thread;
fn main() {
let data = String::from("hello");
// thread borrows data:
let h1 = thread::spawn(move || {
println!("{} world", data);
});
// data moved, main thread can no longer use it
// for sharing across threads:
// use Arc (see sharing section)
// move in a loop:
let mut handles = vec![];
for i in 0..3 {
handles.push(thread::spawn(move || {
println!("thread {}", i);
}));
}
for h in handles {
h.join().unwrap();
}
// closure without move:
// captures by reference, may borrow main
// the thread may outlive the borrow
// use move to transfer ownership explicitly

channel

mpsc is multi-producer single-consumer. send / recv pass data. Type-safe.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::sync::mpsc;
use std::thread;
fn main() {
// create a channel:
let (tx, rx) = mpsc::channel();
// move the sender into the child thread:
thread::spawn(move || {
tx.send(String::from("hello")).unwrap();
});
// receive:
let received = rx.recv().unwrap();
println!("got: {}", received);
// recv blocks until a value arrives
// recv errors when all senders are dropped
// try_recv is non-blocking:
// match rx.try_recv() {
// Ok(v) => ...,
// Err(_) => println!("no data yet"),
// }
// iterate values:
// for v in rx {
// println!("{}", v);
// }

Multiple producers

Clone the Sender for multiple producers. One shared receiver.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
// clone the sender:
let tx1 = tx.clone();
let tx2 = tx.clone();
// three producers:
thread::spawn(move || {
tx.send(1).unwrap();
});
thread::spawn(move || {
tx1.send(2).unwrap();
});
thread::spawn(move || {
tx2.send(3).unwrap();
});
// main thread consumes:
for _ in 0..3 {
let v = rx.recv().unwrap();
println!("got {}", v);
}
// receive order is not guaranteed
// single consumer:
// mpsc allows only one rx
// multi-consumer needs splitting or mutex
// channels are preferred for messaging
// avoid shared mutable state

Shared mutability

Arc<Mutex<T>> shares mutable data across threads. The lock guards access.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// shared counter:
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1; // critical section
}));
}
for h in handles {
h.join().unwrap();
}
println!("result: {}", *counter.lock().unwrap());
// Mutex guarantees exclusion
// Arc makes reference counting thread-safe
// lock guard releases on drop
// keep critical sections short
// avoid taking another lock while holding
// mind the order to prevent deadlocks

scoped threads

thread::scope lets threads borrow non-'static data. No move required.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::thread;
fn main() {
let mut data = vec![1, 2, 3];
// borrow data inside the scope:
thread::scope(|s| {
// immutable borrow:
s.spawn(|| {
println!("{:?}", data);
});
// mutable borrow (separate threads):
s.spawn(|| {
data.push(4); // only this thread uses it
});
}); // scope ends, all threads have joined
println!("{:?}", data);
// vs thread::spawn:
// spawn requires 'static ownership
// scope can borrow local variables
// scope joins automatically when it ends
// fits parallel work over local data
}

rayon parallel

rayon parallel iterators. par_iter replaces iter. Easy data parallelism.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Cargo.toml:
// rayon = "1"
use rayon::prelude::*;
fn main() {
let v: Vec<i32> = (0..1000).collect();
// parallel sum:
let sum: i32 = v.par_iter().sum();
println!("{}", sum);
// parallel map:
let doubled: Vec<i32> =
v.par_iter().map(|x| x * 2).collect();
// parallel filter:
let evens: Vec<&i32> =
v.par_iter().filter(|x| **x % 2 == 0).collect();
// parallel sort:
let mut sorted = vec![3, 1, 2];
sorted.par_sort();
// data must be Send
// auto chunked and parallelized
// big collections benefit clearly
// small collections may be slower
// result matches the serial version

Send & Sync

Send: safe to move across threads. Sync: safe to share behind &T. Compiler-checked.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Send: ownership can cross threads
// most types are Send
// exception: Rc<T> is not Send
// Sync: can be shared via &T
// exception: RefCell is not Sync
use std::rc::Rc;
use std::thread;
fn main() {
// Rc cannot enter a thread:
// let rc = Rc::new(5);
// thread::spawn(move || println!("{}", rc));
// error: Rc is not Send
// use Arc instead:
let arc = std::sync::Arc::new(5);
let h = thread::spawn(move || println!("{}", arc));
h.join().unwrap();
// Rules:
// Send types can cross threads
// Sync types can safely share references
// the compiler checks cross-thread use
// custom types get auto-derived markers
// unsafe impl needs careful review

15.Networking

TCP, UDP, HTTP, and third-party networking crates.

TCP connect

TcpStream connects. read / write exchange bytes. Stream-oriented protocol.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::net::TcpStream;
use std::io::{Read, Write};
fn main() {
// establish connection:
let mut stream = TcpStream::connect(
"example.com:80").unwrap();
// send an HTTP request:
stream.write_all(
b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
.unwrap();
// read the response:
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).unwrap();
println!("{}", String::from_utf8_lossy(&buf[..n]));
// connection error handling:
// TcpStream::connect returns Result
// set timeouts:
// stream.set_read_timeout(Some(Duration::from_secs(5)))
// bidirectional streaming
}

TCP listen

TcpListener binds and accepts. Handle each connection, often in a thread.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use std::thread;
fn main() {
// bind a port:
let listener = TcpListener::bind("127.0.0.1:8080")
.unwrap();
println!("listening on 8080");
// accept connections:
for stream in listener.incoming() {
match stream {
Ok(stream) => {
// one thread per connection:
thread::spawn(|| handle(stream));
}
Err(e) => eprintln!("connection error: {}", e),
}
}
}
fn handle(mut stream: TcpStream) {
let mut buf = [0u8; 1024];
let n = stream.read(&mut buf).unwrap();
println!("received {} bytes", n);
stream.write_all(&buf[..n]).unwrap();
}

HTTP request

reqwest makes HTTP calls. JSON requests and responses.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Cargo.toml:
// reqwest = { version = "0.12", features = ["json"] }
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// GET request:
let resp = reqwest::Client::new()
.get("https://api.example.com/users")
.timeout(Duration::from_secs(10))
.send()
.await?;
// status code:
println!("status: {}", resp.status());
// read body as text:
let body = resp.text().await?;
println!("{}", &body[..200]);
Ok(())
}
// needs the tokio runtime:
// tokio = { version = "1", features = ["full"] }
// JSON:
// .json::<T>() to deserialize
// pair with serde

HTTP server

axum builds HTTP services. Routes and handlers. Async by default.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Cargo.toml:
// axum = "0.7"
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use axum::{Router, routing::get};
use axum::response::Json;
use serde_json::{json, Value};
#[tokio::main]
async fn main() {
// routes:
let app = Router::new()
.route("/", get(root))
.route("/api/ping", get(ping));
// start:
let listener = tokio::net::TcpListener::bind(
"127.0.0.1:3000").await.unwrap();
println!("server running on 3000");
axum::serve(listener, app).await.unwrap();
}
async fn root() -> &'static str {
"Hello, GuruToolkit!"
}
async fn ping() -> Json<Value> {
Json(json!({ "ok": true }))
}
// route parameters:
// .route("/user/{id}", get(user_by_id))

URL handling

The url crate parses and builds URLs. Query parameters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Cargo.toml:
// url = "2"
use url::Url;
fn main() {
// parse:
let url = Url::parse(
"https://[email protected]/path?q=rust#sec")
.unwrap();
// access fields:
println!("{}", url.scheme()); // https
println!("{}", url.host_str().unwrap());
println!("{}", url.path());
println!("{}", url.fragment().unwrap());
// query parameters:
let pairs: Vec<(String, String)> =
url.query_pairs().into_owned().collect();
for (k, v) in pairs {
println!("{}={}", k, v);
}
// build a URL:
let mut u = Url::parse("https://api.com").unwrap();
u.set_path("/v1/users");
u.query_pairs_mut()
.append_pair("page", "2");
println!("{}", u);
}

IP parsing

std::net::IpAddr parses IPs. Distinguish IPv4 vs IPv6.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
fn main() {
// parse an IP:
let ip: IpAddr = "192.168.1.1".parse().unwrap();
println!("{}", ip);
// discriminate version:
match ip {
IpAddr::V4(v) => println!("IPv4: {}", v),
IpAddr::V6(v) => println!("IPv6: {}", v),
}
// construct:
let v4 = Ipv4Addr::new(127, 0, 0, 1);
let v6: Ipv6Addr = "::1".parse().unwrap();
// properties:
println!("loopback: {}", v4.is_loopback());
println!("private: {}", v4.is_private());
// address with port:
let addr = "127.0.0.1:8080".parse::<std::net::SocketAddr>().unwrap();
println!("{}", addr.port());
// resolve a hostname:
// use std::net::ToSocketAddrs;
// let mut it = "example.com:80".to_socket_addrs().unwrap();
}

UDP

UdpSocket is connectionless datagrams. send_to / recv_from. No handshake.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::net::UdpSocket;
fn main() {
// bind locally:
let socket = UdpSocket::bind("127.0.0.1:0")
.unwrap();
// send a datagram:
socket.send_to(b"hello", "127.0.0.1:9999")
.unwrap();
// receive:
let mut buf = [0u8; 1024];
let (n, src) = socket.recv_from(&mut buf).unwrap();
println!("from {}: {}", src,
String::from_utf8_lossy(&buf[..n]));
// server side:
let server = UdpSocket::bind("127.0.0.1:9999")
.unwrap();
// characteristics:
// connectionless, no handshake
// packets may be lost or reordered
// more efficient than TCP
// fits: DNS, video, games
// implement reliability yourself if needed
}

DNS resolution

ToSocketAddrs resolves hostnames. Yields an iterator of addresses.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::net::ToSocketAddrs;
fn main() {
// resolve a hostname:
let addrs = "example.com:80"
.to_socket_addrs().unwrap();
for addr in addrs {
println!("{}", addr);
}
// default port:
let addrs = "example.com"
.to_socket_addrs().unwrap();
// resolution can return multiple addresses
// try them in order when connecting
// error handling:
match "bad-domain.invalid:80".to_socket_addrs() {
Ok(it) => println!("resolved"),
Err(e) => println!("resolution failed: {}", e),
}
// async DNS:
// use third-party crate tokio::net
// in production read resolv.conf
// tests can use a local hosts file

16.Time & Date

SystemTime, Instant, Duration, and chrono.

SystemTime

SystemTime is the system wall clock. Get now and compare against UNIX_EPOCH.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::time::SystemTime;
fn main() {
// current time:
let now = SystemTime::now();
// UNIX epoch:
let epoch = SystemTime::UNIX_EPOCH;
// since the epoch:
match now.duration_since(epoch) {
Ok(d) => println!("since epoch {:?}", d),
Err(e) => println!("system time before epoch: {:?}", e),
}
// comparison:
let later = SystemTime::now();
let elapsed = later.duration_since(now).unwrap();
println!("elapsed {:?}", elapsed);
// limitations:
// not monotonic (system clock may change)
// measure elapsed time with Instant
// calendar handling with chrono
// timestamp:
let secs = now.duration_since(epoch)
.unwrap().as_secs();
println!("{} seconds", secs);
}

Instant timing

Instant is a monotonic clock for elapsed time. Immune to system clock changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::time::Instant;
fn main() {
// start the timer:
let start = Instant::now();
// run a slow operation:
std::thread::sleep(std::time::Duration::from_millis(50));
// compute elapsed time:
let elapsed = start.elapsed();
println!("elapsed: {:?}", elapsed);
// different units:
println!("{} micros", elapsed.as_micros());
println!("{} millis", elapsed.as_millis());
// compare two start points:
let a = Instant::now();
let b = Instant::now();
let diff = b.duration_since(a);
// use cases:
// benchmarks, timeouts
// monotonic, unaffected by clock changes
// cannot compare across processes
}

Duration

Duration is a span of time. Seconds / nanosecond precision. Arithmetic & conversions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::time::Duration;
fn main() {
// constructors:
let sec = Duration::from_secs(5);
let ms = Duration::from_millis(1500);
let us = Duration::from_micros(100);
let ns = Duration::from_nanos(1);
// combined:
let d = Duration::new(2, 500_000_000); // 2.5s
// arithmetic:
let sum = sec + ms;
let mul = sec * 2;
// comparison:
let a = Duration::from_secs(1);
let b = Duration::from_millis(500);
println!("{} > {}", a > b, a > b);
// conversions:
println!("{} secs", sec.as_secs());
println!("{} millis", ms.as_millis());
// uses:
// sleeps, timeouts, waits
// network timeout configuration
}

chrono current time

chrono handles date-times. Utc / Local time zones. Human readable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Cargo.toml:
// chrono = "0.4"
use chrono::{Utc, Local, NaiveDate};
fn main() {
// UTC time:
let utc = Utc::now();
println!("{}", utc);
// local time:
let local = Local::now();
println!("{}", local);
// access fields:
println!("year: {}", utc.year());
println!("month: {}", utc.month());
println!("day: {}", utc.day());
println!("hour: {}", utc.hour());
println!("weekday: {}", utc.weekday());
// timestamp:
println!("{}", utc.timestamp());
// build a date:
let d = NaiveDate::from_ymd_opt(2026, 8, 2)
.unwrap();
println!("{}", d);
// timezone suffix:
// +00:00 UTC / +08:00 China
}

Formatted output

format outputs a date-time. strftime-style placeholders.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// chrono = "0.4"
use chrono::Utc;
fn main() {
let now = Utc::now();
// formatting:
println!("{}", now.format("%Y-%m-%d")); // 2026-08-02
println!("{}", now.format("%H:%M:%S")); // 14:30:22
println!("{}", now.format("%Y-%m-%d %H:%M"));
// common specifiers:
// %Y year %m month %d day
// %H hour %M minute %S second
// %A full weekday %a short weekday
// %B full month name
// %j day of year
// custom:
println!("{}", now.format("%Y/%m/%d %H:%M:%S %z"));
// default Display:
println!("{}", now);
// get a String:
let s = now.format("%F").to_string();
println!("{}", s); // ISO date
}

Parsing strings

DateTime::parse_from_str parses strings. ISO 8601 parsing supported.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// chrono = "0.4"
use chrono::{DateTime, NaiveDateTime, Utc};
fn main() {
// parse a fixed format:
let dt = DateTime::parse_from_str(
"2026-08-02 10:30:00 +0800",
"%Y-%m-%d %H:%M:%S %z").unwrap();
println!("{}", dt);
// ISO 8601:
let iso = DateTime::parse_from_rfc3339(
"2026-08-02T10:30:00+08:00").unwrap();
println!("{}", iso);
// parse without timezone:
let naive = NaiveDateTime::parse_from_str(
"2026-08-02 10:30:00",
"%Y-%m-%d %H:%M:%S").unwrap();
println!("{}", naive);
// convert to UTC:
let utc: DateTime<Utc> = iso.with_timezone(&Utc);
println!("{}", utc);
// parse failure returns Err
}

Time arithmetic

Add/subtract time spans, compute differences, range checks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// chrono = "0.4"
use chrono::{Duration, Utc, Local};
fn main() {
let now = Utc::now();
// add / subtract:
let tomorrow = now + Duration::days(1);
let next_hour = now + Duration::hours(1);
let last_week = now - Duration::weeks(1);
println!("{}", tomorrow.format("%F"));
// interval:
let later = now + Duration::minutes(90);
let diff = later - now;
println!("interval {} minutes", diff.num_minutes());
// date range check:
let start = now - Duration::hours(1);
let end = now + Duration::hours(1);
let now_local = Local::now();
let n = now_local.with_timezone(&Utc);
if n >= start && n <= end {
println!("within range");
}
// timezone:
// FixedOffset::east_opt(8 * 3600)
// China timezone +08:00
}

Time zone handling

FixedOffset represents a fixed time zone. Convert across zones via with_timezone.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// chrono = "0.4"
use chrono::{Utc, Local, FixedOffset, TimeZone};
fn main() {
// fixed offset (China +08:00):
let cst = FixedOffset::east_opt(8 * 3600).unwrap();
let now = Utc::now();
let beijing = now.with_timezone(&cst);
println!("Beijing: {}", beijing);
// local timezone:
let local = Local::now();
println!("local: {}", local);
// convert back to UTC:
let utc = beijing.with_timezone(&Utc);
println!("UTC: {}", utc);
// any timezone:
let tokyo = FixedOffset::east_opt(9 * 3600).unwrap();
let tokyo_now = now.with_timezone(&tokyo);
println!("Tokyo: {}", tokyo_now);
// offset display:
println!("offset: {}", cst); // +08:00
// store data in UTC
// convert when displaying
}

17.Process & Environment

Command-line arguments, environment variables, child processes, and paths.

Arguments & environment

std::env reads args and environment variables. args and var.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::env;
fn main() {
// command-line arguments:
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
// the first one is the program path
// environment variables:
match env::var("PATH") {
Ok(v) => println!("PATH: {}", v),
Err(e) => println!("not set: {:?}", e),
}
// with default:
let port = env::var("PORT")
.unwrap_or_else(|_| "8080".to_string());
// existence check:
// env::var_os("KEY").is_some()
// set an environment variable:
// env::set_var("KEY", "value")
// iterate all:
// for (k, v) in env::vars() {}
}

clap argument parsing

clap parses CLI arguments. Subcommands, options, auto-generated help.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Cargo.toml:
// clap = { version = "4", features = ["derive"] }
use clap::Parser;
// define arguments:
#[derive(Parser, Debug)]
#[command(name = "app", version, about)]
struct Args {
/// user name
#[arg(short, long)]
name: String,
/// iteration count
#[arg(short, long, default_value_t = 1)]
count: u32,
/// verbose output
#[arg(short, long)]
verbose: bool,
}
fn main() {
let args = Args::parse();
println!("{:?}", args);
for _ in 0..args.count {
println!("hello {}", args.name);
}
}
// run:
// app --name Nick --count 3 -v
// auto help: --help
// argument validation is automatic

std::process

Exit codes, Command for child processes, current process info.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::process;
fn main() {
// exit codes:
// process::exit(0) success
// process::exit(1) failure
// current process id:
println!("PID: {}", process::id());
// run a child process:
let out = process::Command::new("ls")
.arg("-l")
.output()
.expect("run failed");
println!("status: {}", out.status);
println!("output: {}",
String::from_utf8_lossy(&out.stdout));
// check success:
if out.status.success() {
println!("command succeeded");
}
// wait for completion:
// process::Command::new("x").status()
}

Command child process

Command controls child processes. Args, env, pipes, capture output.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::process::Command;
use std::io::Write;
fn main() {
// run with arguments:
let mut cmd = Command::new("echo");
cmd.arg("hello").arg("world");
let output = cmd.output().unwrap();
println!("{}",
String::from_utf8_lossy(&output.stdout));
// pipe stdin:
let mut child = Command::new("cat")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn().unwrap();
child.stdin.as_mut().unwrap()
.write_all(b"data").unwrap();
// set environment variable:
let out = Command::new("sh")
.arg("-c")
.arg("echo $MYVAR")
.env("MYVAR", "hello")
.output().unwrap();
println!("{}",
String::from_utf8_lossy(&out.stdout));
}

PathBuf

Path / PathBuf manipulate paths. Join, components, canonicalize. Cross-platform.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::path::{Path, PathBuf};
fn main() {
// build:
let mut p = PathBuf::from("/tmp");
p.push("data");
p.push("file.txt");
println!("{}", p.display());
// components:
println!("parent: {:?}", p.parent());
println!("name: {:?}", p.file_name());
println!("extension: {:?}", p.extension());
// checks:
println!("absolute: {}", p.is_absolute());
println!("exists: {}", p.exists());
// normalize:
let messy = Path::new("./a/../b");
let clean = messy.canonicalize();
// join with formatted string:
let joined = Path::new("/data")
.join(format!("{}.log", 2026));
println!("{}", joined.display());
// interchange with &str:
let s = p.to_string_lossy();
}

Working directory

Current directory, change directory, user home. Path-related helpers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::env;
fn main() {
// current directory:
let cwd = env::current_dir().unwrap();
println!("{}", cwd.display());
// change directory:
// env::set_current_dir("/tmp").unwrap();
// user home directory:
let home = env::home_dir();
match home {
Some(h) => println!("home: {}", h.display()),
None => println!("no home directory"),
}
// temp directory:
let tmp = env::temp_dir();
println!("temp: {}", tmp.display());
// executable path:
let exe = env::current_exe().unwrap();
println!("program: {}", exe.display());
// combine relative paths:
let config = env::current_dir().unwrap()
.join("config")
.join("app.toml");
}

Directory traversal

read_dir lists a directory. Recurse manually. Filter by entry type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::fs;
fn main() {
// read a directory:
let entries = fs::read_dir(".").unwrap();
for entry in entries {
let e = entry.unwrap();
let path = e.path();
// type check:
if path.is_dir() {
println!("[dir] {}", path.display());
} else {
println!("[file] {}", path.display());
}
}
// collect file names:
let files: Vec<String> = fs::read_dir("src")
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.map(|e| e.file_name().to_string_lossy()
.into_owned())
.collect();
println!("{:?}", files);
// recursion needs custom code or the walkdir crate
}

Signal handling

The ctrlc crate catches Ctrl-C. Graceful shutdown, resource cleanup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Cargo.toml:
// ctrlc = "3"
use std::sync::atomic::{
AtomicBool, Ordering
};
use std::sync::Arc;
fn main() {
// catch Ctrl+C:
let running = Arc::new(AtomicBool::new(true));
let r = Arc::clone(&running);
ctrlc::set_handler(move || {
println!("\ninterrupt received, shutting down...");
r.store(false, Ordering::SeqCst);
}).expect("failed to install signal handler");
// main loop:
while running.load(Ordering::SeqCst) {
// handle tasks...
std::thread::sleep(
std::time::Duration::from_millis(100));
}
println!("cleanup done, goodbye");
}
// uses:
// save state, close connections
// graceful service shutdown
// standard library:
// raw signal handling needs libc/unsafe

18.Regular Expressions

The regex crate for matching, capturing, replacing, and splitting.

Compile regex

Regex::new compiles a regex. Use raw strings r#... Reuse compiled regexes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Cargo.toml:
// regex = "1"
use regex::Regex;
fn main() {
// compile:
let re = Regex::new(r"^[a-z]+$").unwrap();
// raw string r"..." avoids escaping
// returns Result, errors on bad pattern
// compilation is slow, reuse it:
// cache with lazy_static/OnceLock
// or thread-local storage
// common patterns:
let email = Regex::new(
r"^[\w.+-]+@[\w-]+\.[\w.]+$").unwrap();
println!("{}", email.is_match("[email protected]"));
// error handling:
match Regex::new(r"[") {
Ok(_) => {}
Err(e) => println!("regex error: {}", e),
}
}

Match check

is_match tests for any match. Use anchors for whole-string matching.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
// contains a match:
println!("{}", re.is_match("abc123")); // true
// partial match by default:
// only checks if it appears anywhere
// full string match:
let full = Regex::new(r"^\d+$").unwrap();
println!("{}", full.is_match("123")); // true
println!("{}", full.is_match("123a")); // false
// find returns the location:
let m = re.find("abc123").unwrap();
println!("position {}..{}", m.start(), m.end());
// case sensitivity:
let case = Regex::new(r"(?i)rust").unwrap();
println!("{}", case.is_match("RUST"));
}

Capture groups

Capture groups extract substrings. captures returns the matches. Named groups supported.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use regex::Regex;
fn main() {
// capture groups:
let re = Regex::new(r"(\d{4})-(\d{2})-(\d{2})")
.unwrap();
let caps = re.captures("date 2026-08-02").unwrap();
// caps[0] is the whole match:
println!("{}", &caps[0]); // 2026-08-02
// caps[1..] are the groups:
println!("year: {}", &caps[1]);
println!("month: {}", &caps[2]);
println!("day: {}", &caps[3]);
// named groups:
let named = Regex::new(r"(?P<year>\d{4})")
.unwrap();
let caps = named.captures("2026").unwrap();
println!("year: {}", caps.name("year").unwrap().as_str());
// no match returns None
// iterate all matches:
// captures_iter yields every match

Find all

find_iter walks every match. captures_iter yields all matches with groups.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
let text = "a1 b22 c333";
// all matches:
let nums: Vec<&str> =
re.find_iter(text).map(|m| m.as_str()).collect();
println!("{:?}", nums); // ["1", "22", "333"]
// iterate with positions:
for m in re.find_iter(text) {
println!("{} at {}..{}", m.as_str(), m.start(), m.end());
}
// all group matches:
let pair = Regex::new(r"(\w+)=(\w+)").unwrap();
for caps in pair.captures_iter("a=1 b=2") {
println!("{} = {}", &caps[1], &caps[2]);
}
// no match returns an empty iterator
}

Replace

replace swaps matches. $1 references groups. replace_all replaces every match.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
// replace all:
let out = re.replace_all("a1 b2", "#");
println!("{}", out); // a# b#
// replace only the first:
let first = re.replace("a1 b2", "#");
println!("{}", first); // a# b2
// group backreferences:
let name = Regex::new(r"(\w+),\s*(\w+)").unwrap();
let out = name.replace_all("Doe, John", "$2 $1");
println!("{}", out); // John Doe
// callback replacement:
let out = re.replace_all("a1 b2", |caps: &regex::Captures| {
let n: i32 = caps[0].parse().unwrap();
format!("{}", n * 2)
});
println!("{}", out); // a2 b4
}

Split

split by regex. splitn limits the number of pieces.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use regex::Regex;
fn main() {
// split by a separator:
let re = Regex::new(r"[,;]+").unwrap();
let parts: Vec<&str> =
re.split("a,b;c,,d").collect();
println!("{:?}", parts); // ["a", "b", "c", "d"]
// split on whitespace:
let ws = Regex::new(r"\s+").unwrap();
let words: Vec<&str> =
ws.split("hello world").collect();
println!("{:?}", words);
// limit the count:
let parts2: Vec<&str> = re.splitn("a,b,c", 2).collect();
println!("{:?}", parts2); // ["a", "b,c"]
// keep the matching separators:
// let parts: Vec<&str> =
// re.split_inclusive("a,b", 0).collect();
// simple splits can use str::split for speed
// reach for regex on complex patterns

Common patterns

Common regex recipes: email, URL, phone, IP. Character classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use regex::Regex;
fn main() {
// email:
let email = Regex::new(
r"^[\w.+-]+@[\w-]+\.[\w.]+$").unwrap();
println!("{}", email.is_match("[email protected]"));
// URL:
let url = Regex::new(r"^https?://[\w.-]+(/\S*)?$").unwrap();
println!("{}", url.is_match("https://example.com"));
// phone (China):
let phone = Regex::new(r"^1[3-9]\d{9}$").unwrap();
println!("{}", phone.is_match("13800138000"));
// character classes:
// \d digit \w word \s whitespace
// \D \W \S negated forms
// + one or more * zero or more
// {2,4} count {2,} at least two
// ^ start $ end
// [a-z] [^0-9] character ranges
// greedy? add ? to be non-greedy: \d+?
// grouping ( ) or alternation |
}

Flags & case

(?i) inline flags like ignore-case. RegexBuilder for more control.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use regex::Regex;
fn main() {
// ignore case:
let re = Regex::new(r"(?i)rust").unwrap();
println!("{}", re.is_match("RUST"));
println!("{}", re.is_match("rust"));
// multi-line:
// (?m) makes ^ $ match every line
let m = Regex::new(r"(?m)^go").unwrap();
println!("{}", m.is_match("x\ngo"));
// dot matches newline:
// (?s) lets . match \n
// without inline flags:
// use RegexBuilder for configuration
let re = regex::RegexBuilder::new("rust")
.case_insensitive(true)
.multi_line(true)
.build().unwrap();
println!("{}", re.is_match("RUST"));
// single-line:
// .size_limit() bounds complexity
// UTF-8 by default, switch off via bytes
}

19.Build & Test

cargo build, unit tests, linting, and release configuration.

cargo build

Build the project. Debug / release profiles. Output paths. Incremental compilation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// debug build:
// cargo build
// release build (optimized):
// cargo build --release
// check (no binary):
// cargo check
// artifacts:
// target/debug/ target/release/
// specific package:
// cargo build -p mylib
// incremental compilation:
// unchanged files are not recompiled
// feature selection:
// cargo build --features xxx
// clean:
// cargo clean
// run:
// cargo run
// run the binary directly:
// ./target/debug/app
// build options live in Cargo.toml [profile]

Unit tests

#[test] attribute. cargo test runs them. Assertion macros.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// unit tests:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_sub() {
assert!(sub(2, 1) == 1);
}
#[test]
#[should_panic] // expect panic
fn test_bad() {
panic!("boom");
}
}
fn add(a: i32, b: i32) -> i32 { a + b }
fn sub(a: i32, b: i32) -> i32 { a - b }
// run all:
// cargo test
// run one:
// cargo test test_add
// show output:
// cargo test -- --nocapture
// assertion macros:
// assert_eq! / assert_ne!
// assert! with message

Doc tests

Doc comments can run as tests. cargo test verifies them. Integration tests live in tests/.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// Adds two numbers.
///
/// # Examples
/// ```
/// let result = add(1, 2);
/// assert_eq!(result, 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// doc tests:
// example code is compiled and run
// cargo test verifies them
// generate docs:
// cargo doc --open
// integration tests:
// tests/integration.rs
// call public APIs directly
// run: cargo test
// doc comment syntax:
// /// doc comment for an item
// //! module-level documentation
// ```rust,ignore to skip
// ```text no compilation

Dependency management

Cargo.toml declares dependencies. Semver. cargo update.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Cargo.toml dependencies:
// [dependencies]
// serde = "1" // any 1.x compatible
// serde = "1.0.100" // exact lower bound
// serde = "=1.0.100" // exact version
// serde = "^1.2" // caret range
// optional dependency:
// [dependencies]
// serde = { version = "1", optional = true }
// dev dependency:
// [dev-dependencies]
// pretty_assertions = "1"
// maintenance:
// cargo update // refresh lockfile
// cargo add serde // add dependency
// cargo remove serde // remove
// check for stale deps:
// cargo outdated
// audit for known vulns:
// cargo audit

clippy lint

cargo clippy lints code. Catches potential bugs and style issues.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// run lint:
// cargo clippy
// strict mode:
// cargo clippy -- -D warnings
// common checks:
// 1. needless clones
// 2. simplifiable patterns
// 3. performance traps
// 4. suspicious logic
// example lint:
// let v = vec![1, 2];
// if v.len() > 0 {} // clippy suggests
// if !v.is_empty() {} // better
// allow a lint:
// #[allow(clippy::all)]
// locally:
// #[allow(clippy::redundant_clone)]
// config file:
// clippy.toml
// CI commonly uses -D warnings
// suggestions like & over .iter() are common

Code formatting

cargo fmt applies rustfmt. Consistent style across the team.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// format:
// cargo fmt
// check format (no changes):
// cargo fmt --check
// config file:
// rustfmt.toml
// common options:
// max_width = 100
// tab_spaces = 4
// features:
// automatic indent and alignment
// import sorting (nightly only):
// cargo +nightly fmt
// CI check:
// cargo fmt --check
// with clippy:
// fmt first, then clippy
// editor save hook
// consistent team style
// reduce diff churn

Release configuration

[profile.release] tweaks optimization. Strip, LTO, panic behavior.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Cargo.toml:
// [profile.release]
// opt-level = 3 // optimization level
// lto = true // link-time optimization
// codegen-units = 1 // single codegen unit
// panic = "abort" // abort on panic
// strip = "symbols" // strip symbols
// debug = false // no debug info
// small binary combo:
// [profile.release]
// lto = true
// strip = "symbols"
// opt-level = "z" // favor size
// build:
// cargo build --release
// inspect size:
// ls -lh target/release/app
// before release:
// cargo fmt --check
// cargo clippy -- -D warnings
// cargo test
// version number:
// Cargo.toml version field

Documentation generation

cargo doc generates API docs. rustdoc markup. Run doc tests before release.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// generate docs:
// cargo doc
// open in browser:
// cargo doc --open
// doc comments:
/// Sum of two numbers.
///
/// # Arguments
/// - `a`: the first number
/// - `b`: the second number
///
/// # Returns
/// the sum
///
/// # Examples
/// ```
/// assert_eq!(add(1, 2), 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// common doc sections:
// # Examples # Panics
// # Errors # Safety
// module doc: //!
// intra-doc link: \[`add`\]
// private items are skipped
// run doc tests before release

Official Links

Direct links to the official docs and resources.

About this Cheatsheet

This page is a self-contained Rust 2021 cheatsheet — covering the language core and the most common std APIs that handle about 80% of everyday usage in real projects. The content favors modern idioms: ? for error propagation, exhaustive match, iterators over hand-written loops, and a clear distinction between String and &str. For authoritative references see the official Rust Book and the standard library docs. The 19 sections each focus on one topic — from your first program to ownership, traits, concurrency, and common pitfalls. Each section is broken into 8 example-driven subtopics (5–20 lines each), totalling roughly 150 topics. The code snippets are deliberately short and self-explanatory. Everything runs in your browser — nothing is uploaded, nothing is tracked. This page is part of GuruToolkit's free developer toolset; the snippets here are free to use with no warranty.

Version 2.1.0