Rust logoRustv1.97ADVANCED

Rust

Comprehensive Rust reference covering ownership, borrowing, structs, enums, traits, generics, error handling, async/await, concurrency, macros, and more.

15 min read
rustsystemsmemory-safetyconcurrencyperformance

Sign in to mark items as known and track your progress.

Sign in

Getting Started

Install Rust, create a project with Cargo, and run your first program.

Installation & Cargo

Install Rust with rustup and use Cargo to create and run projects.

rust
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Create a new project
cargo new my_project
cd my_project

# Build and run
cargo run

# Hello World — src/main.rs
fn main() {
    println!("Hello, world!");
}
💡 Use "cargo check" instead of "cargo build" for fast type-checking during development
⚡ cargo clippy catches common mistakes and suggests idiomatic improvements
📌 println! uses {} for Display, {:?} for Debug, and {:#?} for pretty-printed Debug
🟢 Add dependencies to Cargo.toml — Cargo downloads and compiles them automatically
installcargosetup

Variables & Types

Variables, mutability, basic types, tuples, arrays, and type aliases.

Variables & Basic Types

Declare variables with let, control mutability, and use primitive types.

rust
// Immutable by default
let x = 5;
let name = "Alice";

// Mutable
let mut count = 0;
count += 1;

// Type annotations
let age: u32 = 30;
let pi: f64 = 3.14;
let active: bool = true;
let letter: char = 'A';

// Constants
const MAX_SIZE: u32 = 100;
💡 Variables are immutable by default — add mut only when you need to change the value
⚡ Shadowing lets you reuse a name with a different type — unlike mut which keeps the type
📌 Use underscores in numeric literals for readability: 100_000 instead of 100000
🟢 Tuples destructure with let (x, y) = tup; arrays access with arr[0]
variablestypesmutability

Strings

Understand &str vs String, conversions, and string operations.

&str vs String

The two string types, when to use each, and how to convert between them.

rust
// &str — string slice (borrowed, immutable, stack)
let greeting: &str = "hello";

// String — owned, growable, heap-allocated
let mut s = String::from("hello");
s.push_str(", world!");
s.push('!');

// Conversions
let owned: String = "hello".to_string();
let slice: &str = &owned;
💡 Use &str for function parameters — it accepts both &str and &String (via deref coercion)
⚡ format!() creates a new String — like println! but returns instead of printing
📌 .len() returns bytes, not characters — use .chars().count() for Unicode char count
🟢 Rule of thumb: accept &str, return String — gives callers maximum flexibility
stringsstrstringconversions

Control Flow

If expressions, loops, match, if let, and pattern matching.

If, Loops & Match

Conditional expressions, loops, and pattern matching.

rust
// If expression (returns a value)
let status = if age >= 18 { "adult" } else { "minor" };

// Loops
for item in &items { println!("{item}"); }
for i in 0..5 { println!("{i}"); }        // 0,1,2,3,4
while count > 0 { count -= 1; }
let result = loop { break 42; };          // loop returns a value

// Match
match value {
    1 => println!("one"),
    2 | 3 => println!("two or three"),
    4..=9 => println!("four to nine"),
    _ => println!("other"),
}
💡 if and match are expressions — they return values, so you can assign them to variables
⚡ if let is sugar for matching a single pattern — cleaner than a full match for Option/Result
📌 let-else (let Some(x) = val else { return }) is the idiomatic "unwrap or bail" pattern
🟢 Match is exhaustive — the compiler forces you to handle every possible case
ifloopsmatchpattern-matching

Functions & Closures

Define functions, closures, and understand Fn traits.

Functions & Closures

Named functions, closures, and the Fn/FnMut/FnOnce traits.

rust
// Function with parameters and return type
fn add(a: i32, b: i32) -> i32 {
    a + b    // No semicolon = return value
}

// Closure — anonymous function
let double = |x: i32| x * 2;
let sum = |a, b| a + b;
let result = double(5);  // 10

// Closure capturing variables
let name = String::from("Alice");
let greet = || println!("Hello, {name}");
💡 No semicolon on the last expression makes it the return value — this is idiomatic Rust
⚡ Closures infer types from usage — you rarely need type annotations on them
📌 Use move || to force a closure to take ownership — required for spawning threads
🟢 Fn (immutable borrow) > FnMut (mutable borrow) > FnOnce (ownership) — most closures are Fn
functionsclosuresfn-traits

Ownership & Borrowing

Rust's ownership system, borrowing rules, and Clone vs Copy.

Ownership & Borrowing

Move semantics, borrowing rules, and the Clone/Copy distinction.

rust
// Ownership — each value has one owner
let s1 = String::from("hello");
let s2 = s1;          // s1 is MOVED to s2, s1 is no longer valid
// println!("{s1}");   // ERROR: value moved

// Clone — explicit deep copy
let s1 = String::from("hello");
let s2 = s1.clone();  // Deep copy, both valid

// Borrowing — references don't take ownership
fn print_len(s: &String) { println!("{}", s.len()); }
print_len(&s2);        // Borrow s2, s2 still valid

// Mutable reference
fn push_world(s: &mut String) { s.push_str(" world"); }
💡 Copy types (i32, f64, bool, char) are copied on assignment; heap types (String, Vec) are moved
⚡ The rule: either one &mut OR many & — never both at the same time
📌 When a function takes a value (not a reference), ownership is moved and the caller loses it
🟢 Use .clone() when you need two owners — but prefer borrowing when possible
ownershipborrowingreferencesmoveclone

Structs & Enums

Define custom types with structs and enums, and attach methods with impl.

Structs & Methods

Define structs and attach methods with impl blocks.

rust
#[derive(Debug)]
struct User {
    name: String,
    age: u32,
    active: bool,
}

impl User {
    // Associated function (constructor)
    fn new(name: &str, age: u32) -> Self {
        Self { name: name.to_string(), age, active: true }
    }

    // Method (takes &self)
    fn is_adult(&self) -> bool {
        self.age >= 18
    }
}

let user = User::new("Alice", 30);
println!("{:?}", user);
💡 &self borrows, &mut self borrows mutably, self takes ownership — pick the least powerful one
⚡ Self is an alias for the struct type inside impl blocks — use it in constructors
📌 Struct update syntax (..other) moves fields from other — clone first if you still need it
🟢 Convention: use new() as the constructor name, like User::new()
structsmethodsimpl

Enums

Define enums with variants that can hold data.

rust
// Enum with data variants
enum Shape {
    Circle(f64),                    // radius
    Rectangle { width: f64, height: f64 },
    Triangle(f64, f64, f64),        // sides
}

// Use match to handle variants
fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle { width, height } => width * height,
        Shape::Triangle(a, b, c) => { /* ... */ 0.0 }
    }
}
💡 Option<T> replaces null — Some(value) or None, compiler forces you to handle both
⚡ Use matches!(val, Pattern) for quick boolean pattern checks without a full match
📌 Each enum variant can hold different types and amounts of data — much more powerful than C enums
🟢 .unwrap() panics on None — prefer .unwrap_or(), .map(), or if let in production code
enumsoptionmatchvariants

Traits

Define shared behavior with traits, derive common traits, and use From/Into.

Defining & Implementing Traits

Create traits, implement them for types, and use trait bounds.

rust
// Define a trait
trait Summary {
    fn summarize(&self) -> String;

    // Default implementation
    fn preview(&self) -> String {
        format!("{}...", &self.summarize()[..20])
    }
}

// Implement for a type
impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} by {}", self.title, self.author)
    }
}
💡 impl Trait in parameters = static dispatch (monomorphized); dyn Trait = dynamic dispatch (vtable)
⚡ Use where clauses when trait bounds get complex — much more readable than inline bounds
📌 Trait objects (Box<dyn Trait>) let you store different types in one collection
🟢 impl Trait in return position means "returns some type implementing Trait" — great for closures
traitsboundsdynimpl-trait

Common Traits & Derive

Derive traits automatically and implement From/Into for conversions.

rust
// Derive common traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Point { x: i32, y: i32 }

// From/Into — type conversions
impl From<(i32, i32)> for Point {
    fn from((x, y): (i32, i32)) -> Self {
        Point { x, y }
    }
}
let p: Point = (10, 20).into();

// Display — custom printing with {}
impl std::fmt::Display for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}
💡 Implementing From<T> gives you Into<T> for free — always implement From, not Into
⚡ #[derive(Default)] gives you Config::default() — all fields use their type's default (0, "", false)
📌 Copy can only be derived if all fields are Copy — String is not Copy, so structs with String can't be
🟢 Operator overloading uses traits from std::ops — Add for +, Sub for -, Mul for *, Index for []
derivefromintodisplayoperator-overloading

Generics & Lifetimes

Write generic code and annotate lifetimes for references.

Generics & Lifetimes

Generic functions, structs, and lifetime annotations.

rust
// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut max = &list[0];
    for item in list { if item > max { max = item; } }
    max
}

// Generic struct
struct Wrapper<T> { value: T }

// Lifetime annotation — tells Rust how long references live
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
💡 Lifetimes don't change how long data lives — they help the compiler verify references are valid
⚡ Most lifetimes are inferred (elision rules) — you only annotate when the compiler asks
📌 'static means the reference can live for the entire program — string literals are 'static
🟢 Rule: if a function returns a reference, it must come from an input — not from local data
genericslifetimesbounds

Error Handling

Handle errors with Result, Option, the ? operator, and custom error types.

Result, Option & the ? Operator

Propagate errors idiomatically with Result<T, E> and the ? operator.

rust
// Result<T, E> — success or error
fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
    let port = s.parse::<u16>()?;   // ? returns early on error
    Ok(port)
}

// Handle Result
match parse_port("8080") {
    Ok(port) => println!("Port: {port}"),
    Err(e) => eprintln!("Error: {e}"),
}

// ? chains multiple fallible operations
fn read_config() -> Result<String, std::io::Error> {
    let contents = std::fs::read_to_string("config.toml")?;
    Ok(contents)
}
💡 The ? operator replaces verbose match/unwrap patterns — it's the idiomatic way to propagate errors
⚡ Implement From<OtherError> for your error type — then ? auto-converts between error types
📌 Use Box<dyn Error> as a quick return type when you don't want a custom error enum
🟢 In production, use the anyhow crate for applications and thiserror for libraries
resultoptionerror-handlingquestion-mark

Collections & Iterators

Vec, HashMap, and iterator methods for transforming data.

Vec & HashMap

Dynamic arrays and key-value maps.

rust
// Vec — dynamic array
let mut v = vec![1, 2, 3];
v.push(4);
v.pop();              // Some(4)
let first = &v[0];   // Panics if out of bounds
let first = v.get(0); // Returns Option<&T>

// HashMap
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("Alice", 30);
map.get("Alice");     // Some(&30)
💡 Use v.get(i) instead of v[i] to avoid panics — returns Option<&T>
⚡ The entry().or_insert() pattern is the idiomatic way to insert-if-absent in HashMaps
📌 .collect() can build Vec, HashMap, String, and more — the target type determines behavior
🟢 Use &v to borrow during iteration, v to consume, &mut v to mutate in place
vechashmapcollections

Iterators

Transform collections with iterator adaptors and consumers.

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

// Map, filter, collect
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
let evens: Vec<&i32> = nums.iter().filter(|x| *x % 2 == 0).collect();

// Chaining
let result: i32 = nums.iter()
    .filter(|x| *x % 2 != 0)
    .map(|x| x * x)
    .sum();
💡 Iterators are lazy — .map() and .filter() do nothing until you .collect() or .sum()
⚡ .iter() borrows, .into_iter() consumes, .iter_mut() mutably borrows the collection
📌 Use turbofish ::<Vec<_>> with .collect() when the compiler can't infer the target type
🟢 Chain adaptors freely — Rust optimizes iterator chains to be as fast as hand-written loops
iteratorsmapfiltercollect

Smart Pointers

Box, Rc, Arc, RefCell, and Cow for advanced memory management.

Box, Rc, Arc & RefCell

Heap allocation, reference counting, and interior mutability.

rust
// Box<T> — heap allocation
let b = Box::new(5);
// Used for: recursive types, large data, trait objects

// Rc<T> — multiple owners (single-threaded)
use std::rc::Rc;
let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a);  // Both a and b own the data

// Arc<T> — multiple owners (thread-safe)
use std::sync::Arc;
let data = Arc::new(vec![1, 2, 3]);

// RefCell<T> — interior mutability (runtime borrow checking)
use std::cell::RefCell;
let cell = RefCell::new(5);
*cell.borrow_mut() += 1;
💡 Box is for heap allocation — use it for recursive types, large data, and dyn Trait
⚡ Rc is for multiple owners in one thread; Arc is the thread-safe version — same API
📌 RefCell moves borrow checking from compile time to runtime — panics if rules are violated
🟢 Cow<str> avoids cloning when you might or might not need to modify a string
boxrcarcrefcellcowsmart-pointers

Concurrency

Threads, channels, Arc, and Mutex for safe concurrent programming.

Threads, Channels & Mutex

Spawn threads, pass messages, and share state safely.

rust
use std::thread;
use std::sync::mpsc;

// Spawn a thread
let handle = thread::spawn(|| {
    println!("Hello from a thread!");
});
handle.join().unwrap();

// Channel — message passing
let (tx, rx) = mpsc::channel();
thread::spawn(move || { tx.send("hello").unwrap(); });
let msg = rx.recv().unwrap();
💡 Arc<Mutex<T>> is the standard pattern for shared mutable state across threads
⚡ Channels (mpsc) are for message passing; Mutex is for shared state — pick one paradigm
📌 Mutex::lock() returns a MutexGuard — it auto-unlocks when the guard is dropped
🟢 Send + Sync traits are auto-implemented — the compiler prevents unsafe sharing at compile time
threadschannelsmutexarcconcurrency

Async Programming

Async/await with tokio for non-blocking I/O.

Async/Await & Tokio

Write async functions and use the tokio runtime.

rust
// Add to Cargo.toml:
// [dependencies]
// tokio = { version = "1", features = ["full"] }

#[tokio::main]
async fn main() {
    let result = fetch_data().await;
    println!("{result}");
}

async fn fetch_data() -> String {
    // .await pauses until the future completes
    "data".to_string()
}
💡 async fn returns a Future — nothing happens until you .await it
⚡ tokio::join! runs futures concurrently; sequential .await runs them one after another
📌 Use tokio::sync::Mutex (not std::sync::Mutex) in async code — it doesn't block the runtime
🟢 tokio::spawn creates lightweight tasks — much cheaper than OS threads
asyncawaittokiofutures

Macros & File I/O

Define macros with macro_rules! and read/write files with std::fs.

Macros

Write declarative macros with macro_rules! for code generation.

rust
// Built-in macros
println!("Hello, {name}!");
format!("x = {x}");
vec![1, 2, 3];
todo!("implement later");         // Compiles but panics at runtime
unimplemented!("not yet");
dbg!(expression);                  // Prints file:line and value

// Custom macro
macro_rules! say_hello {
    () => { println!("Hello!"); };
    ($name:expr) => { println!("Hello, {}!", $name); };
}
💡 dbg!() prints the expression, file, and line — perfect for quick debugging
⚡ todo!() lets you leave unfinished code that compiles — panics if actually reached
📌 Macros use ! to distinguish them from functions — vec![], println!(), assert_eq!()
🟢 The hashmap!{} macro pattern is a common way to create utility macros for collections
macrosmacro-rulesattributes

File I/O

Read and write files with std::fs.

rust
use std::fs;

// Read entire file to string
let contents = fs::read_to_string("data.txt")?;

// Write string to file (creates or overwrites)
fs::write("output.txt", "Hello, world!")?;
💡 fs::read_to_string() and fs::write() are the quickest way to read/write files
⚡ Use BufReader for line-by-line reading — much more memory efficient than read_to_string
📌 All fs operations return Result — use ? to propagate errors
🟢 Use PathBuf::from("dir").join("file") for cross-platform path construction
filesiofspath

Modules & Cargo

Organize code with modules and manage dependencies with Cargo.

Modules & Visibility

Organize code into modules with pub/use and file-based structure.

rust
// Inline module
mod utils {
    pub fn helper() -> String {
        "help".to_string()
    }
}
use utils::helper;

// File-based modules:
// src/main.rs    — declares: mod routes;
// src/routes.rs  — or src/routes/mod.rs
💡 Everything is private by default — add pub to make it accessible outside the module
⚡ pub(crate) makes something public within your crate but hidden from external users
📌 Module files: mod routes; looks for src/routes.rs or src/routes/mod.rs
🟢 Use pub use for re-exports — lets users import from a convenient path
modulesvisibilityusecargo

Testing

Write unit tests, integration tests, and use assert macros.

Writing Tests

Unit tests, integration tests, and test organization.

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

    #[test]
    fn it_works() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    #[should_panic(expected = "divide by zero")]
    fn panics_on_zero() {
        divide(1, 0);
    }
}

// Run: cargo test
💡 #[cfg(test)] ensures test code is never compiled into your release binary
⚡ Tests that return Result<(), E> let you use ? instead of .unwrap() — cleaner test code
📌 Use -- --nocapture to see println! output from passing tests
🟢 Integration tests go in tests/ directory and can only access your public API
testingassertunit-tests