1. Overview & Questions (SQ3R · Survey & Question)

SQ3R Step 1: Skim the big picture, formulate key questions.

What Is Rust?

Rust is a systems programming language sponsored by Mozilla and now maintained by the Rust Foundation. Since its 1.0 release in 2015, it has been voted the "most loved programming language" in the Stack Overflow Developer Survey for consecutive years. Rust's core design pursues three seemingly contradictory promises: memory safety, zero-cost abstractions, and fearless concurrency — without a garbage collector (GC) and without manual memory management.

Rust's philosophy can be summarized in a single sentence: catch at compile time the bugs that other languages only expose at runtime. Its ownership system, borrow checker, and type system form a safety net that helps the compiler find potential memory errors and data races before you ever run your code.

Key Questions

  • Where does Rust shine? — Operating system components, WebAssembly, embedded development, network services, CLI tools, blockchain infrastructure, game engines — anywhere that demands both high performance and safety.
  • What advantages does Rust have over C/C++/Go? — Compared to C/C++, Rust guarantees memory safety and thread safety without sacrificing performance. Compared to Go, Rust offers finer-grained memory control, higher runtime performance, and zero-cost abstractions without the overhead of a garbage collector.
  • What is the hardest part of learning Rust? — The ownership system and lifetimes. These are Rust's most unique features and the source of most beginner compile errors. But once understood, they fundamentally change how you think about code.

Technology Landscape

Rust's knowledge architecture can be understood in five core layers:

Layer 1: Basic Syntax — Variables and mutability, data types (scalar and compound), functions, control flow (if/loop/while/for), comments.

Layer 2: The Ownership System — Ownership rules (one owner per value, dropped when owner leaves scope), move vs. clone, references and borrowing (&/&mut), the slice type. This is the soul of Rust.

Layer 3: Data Abstraction — Structs, enums and pattern matching (match/if let), the trait system (similar to interfaces but more powerful), generics.

Layer 4: Error Handling & CollectionsResult<T, E> and Option<T>, panic!, the ? operator, Vector, String, HashMap.

Layer 5: Advanced Features — Lifetimes, smart pointers (Box/Rc/Arc/RefCell), closures and iterators, concurrency (threads/channels/Mutex/Arc/Send/Sync), async/await, macros, unsafe Rust, the module system.

2. Explained Simply (Feynman Technique)

Feynman Technique core idea: If you can't explain something in simple language, you don't truly understand it.

Core Concepts Explained

Ownership

Imagine you own a book. You are the "owner" of that book. If you give the book to someone else, you no longer own it — you can't read it anymore because it's no longer yours. This is exactly how Rust's ownership system works.

Rust's ownership rules are just three:

  1. Each value in Rust has a single owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped.
let s1 = String::from("hello");
let s2 = s1; // Ownership of s1 moves to s2
// println!("{s1}"); // Compile error! s1 is no longer valid
println!("{s2}"); // Works fine

Why this design? Because a String's data lives on the heap. If both s1 and s2 pointed to the same heap memory, they would both try to free it when they go out of scope — a "double free" that corrupts memory. Rust's solution is simple and elegant: after the move, the original variable is automatically invalidated.

For simple types that live on the stack (like i32, f64, bool, char), Rust performs a copy instead of a move, because copying these types is cheap:

let x = 5;
let y = x; // i32 implements the Copy trait, so x is still valid
println!("x = {x}, y = {y}"); // No problem at all

Borrowing & References

If your friend wants to read your book but you don't want to give it away, you can "lend" it to them. They read it and give it back, but ownership never changes. This is "borrowing" in Rust.

fn calculate_length(s: &String) -> usize { // s borrows the String
    s.len()
} // s goes out of scope, but since it doesn't own the data, nothing is dropped
 
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // borrow s1
    println!("The length of '{s1}' is {len}."); // s1 is still valid
}

The core rules of borrowing:

  • At any given time, you can have either any number of immutable references (&T) or exactly one mutable reference (&mut T), but not both.
  • References must always be valid (no dangling references allowed).
let mut s = String::from("hello");
 
let r1 = &s;     // OK
let r2 = &s;     // OK — multiple immutable references are allowed
// let r3 = &mut s; // Compile error! Cannot create a mutable reference
                     // while immutable references exist
println!("{r1}, {r2}");
 
let r3 = &mut s; // OK — r1 and r2 are no longer in use
println!("{r3}");

This rule may seem strict, but its significance is profound: it eliminates data races at compile time. A data race occurs when three conditions are met simultaneously: two or more pointers access the same data, at least one is writing, and there is no synchronization mechanism. Rust's borrowing rules break these conditions at the root.

Lifetimes

Lifetimes are Rust's mechanism for tracking how long references remain valid. Every reference has a lifetime — the scope for which it is valid. Most of the time, lifetimes are inferred implicitly, but when the compiler can't determine them, you must annotate explicitly.

// Tell the compiler: the returned reference lives at least as long as the inputs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
 
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
        println!("Longest string: {result}");
    }
    // Using result here would be a compile error, since string2 was dropped
}

The essence of lifetimes is not about extending how long references live — it's about letting the compiler verify that references never outlive the data they point to.

Enums & Pattern Matching

Rust's enums are far more powerful than in other languages. Each variant can carry different types and amounts of data:

enum Message {
    Quit,                       // No data
    Move { x: i32, y: i32 },   // Named fields, like a struct
    Write(String),              // Contains a String
    ChangeColor(i32, i32, i32), // Contains three i32s
}
 
fn process(msg: Message) {
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to ({x}, {y})"),
        Message::Write(text) => println!("Write: {text}"),
        Message::ChangeColor(r, g, b) => println!("Color: ({r}, {g}, {b})"),
    }
}

match is exhaustive — if you miss any variant, the compiler will error. You will never forget to handle a case.

Option<T> — Rust has no null. Instead, it uses Option<T> to represent "a value that may or may not exist":

let some_number: Option<i32> = Some(42);
let no_number: Option<i32> = None;
 
// You can't use Option<i32> directly as i32
// let sum = some_number + 5; // Compile error! Must handle None first
 
match some_number {
    Some(n) => println!("Value is: {n}"),
    None => println!("No value"),
}

Tony Hoare, the inventor of null, called it his "billion-dollar mistake." Rust uses the type system to move this problem from runtime to compile time.

Error Handling: Result & ?

Rust categorizes errors into two types: recoverable errors (like a missing file) and unrecoverable errors (like array out-of-bounds access).

  • Unrecoverable errors use the panic! macro, which crashes the program.
  • Recoverable errors use the Result<T, E> type:
use std::fs::File;
use std::io::{self, Read};
 
fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();
    File::open("username.txt")?.read_to_string(&mut username)?;
    Ok(username)
}

The ? operator is the essence of Rust error handling: if the Result is Ok(v), it yields v; if it's Err(e), it immediately returns Err(e) from the current function. This makes error handling both explicit and concise — you always see which operations can fail, without writing verbose match blocks.

The Trait System

Traits define shared behavior — similar to interfaces in other languages, but more powerful:

pub trait Summary {
    fn summarize_author(&self) -> String;
 
    // Default implementations can call other trait methods
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}
 
pub struct Article {
    pub title: String,
    pub author: String,
}
 
impl Summary for Article {
    fn summarize_author(&self) -> String {
        format!("@{}", self.author)
    }
    // summarize uses the default implementation
}

Traits can also be used as function parameters and return types:

// impl Trait syntax — accepts any type that implements Summary
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}
 
// Trait bound syntax — more flexible constraint form
pub fn notify_bound<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}
 
// where clause — clearer when constraints get complex
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: std::fmt::Display + Clone,
    U: Clone + std::fmt::Debug,
{
    unimplemented!()
}

An important trait limitation is the orphan rule: you can implement a trait on a type only if at least one of them (the trait or the type) is local to your crate. This prevents implementation conflicts between different crates.

Analogies & Metaphors

  • Ownership = a property deed. A house has one owner at a time. If you sell it (move), you're no longer the owner.
  • Borrowing = lending a book. Ownership doesn't change; you temporarily hold it. You can read (immutable borrow) or annotate (mutable borrow) the book, but you can't lend it to two people for annotation at the same time.
  • Lifetimes = a lease. Your lease can't outlast the building itself. The compiler is the landlord checking all leases.
  • Option<T> = a safe mystery box. You either open it and find something (Some(T)), or it's empty (None). But you can't pretend there's definitely something inside.
  • Result<T, E> = a delivery. Either the package arrives successfully (Ok(T)), or it comes with an error notice (Err(E)). You must check at the door.

Common Misconceptions

  1. "Rust is too hard" — Rust's learning curve is steep, but the difficulty is concentrated in the first few weeks. Once you develop an ownership mindset, the compiler becomes your best teacher — it catches problems at compile time, not at 3 AM in production.
  2. "Rust is as low-level as C++" — While Rust can do low-level development, its high-level abstractions (iterators, closures, pattern matching, traits) feel like a modern high-level language. Zero-cost abstractions mean you don't pay a runtime penalty for these conveniences.
  3. "No GC means manual memory management" — Quite the opposite. Rust's ownership system automatically determines when to free each piece of memory at compile time. You never call free or delete. It's neither GC nor manual management — it's a third path.

3. Cone of Depth (Simon Learning Method)

Simon Learning Method: Focus intensely, drilling deep in one direction like a cone.

Layer 1: Core Fundamentals

Variables & Mutability

Variables in Rust are immutable by default. This is a deliberate design choice: immutability makes code easier to reason about and works hand-in-hand with the ownership system. When mutability is needed, declare it explicitly:

let x = 5;          // immutable
// x = 6;           // compile error
let mut y = 5;      // mutable
y = 6;              // fine
 
let z = 5;          // shadowing
let z = z + 1;      // creates a new variable, doesn't modify the original
let z = z * 2;      // can even change type
let spaces = "   ";
let spaces = spaces.len(); // changed from &str to usize — shadowing allows this

The difference between const and let: constants use const, require type annotations, cannot use mut, and cannot be computed at runtime:

const MAX_POINTS: u32 = 100_000;

Data Types

Scalar types:

// Integers: i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, isize, usize
let a: i32 = -42;          // signed 32-bit
let b: u8 = 255;           // unsigned 8-bit
let c = 98_222;            // numeric readability separator
let d = 0xff;              // hexadecimal
let e = 0o77;              // octal
let f = 0b1111_0000;       // binary
let g = b'A';              // byte (u8)
 
// Floating-point
let pi: f64 = 3.14159;     // f64 is default, speed similar to f32 on modern CPUs
 
// Boolean
let t: bool = true;
 
// Character — Unicode scalar value
let heart = '❤';
let z = 'ℤ';

Compound types:

// Tuple — fixed length, can have different types
let tup: (i32, f64, bool) = (500, 6.4, true);
let (x, y, z) = tup;       // destructuring
let first = tup.0;         // index access
 
// Array — fixed length, same type
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let first = arr[0];
let repeat = [3; 5];       // [3, 3, 3, 3, 3]

Functions

fn add(x: i32, y: i32) -> i32 {
    x + y // the last expression is the return value (no semicolon)
    // x + y; // with a semicolon, this becomes a statement returning (),
              // causing a compile error
}
 
fn print_sum(x: i32, y: i32) {
    println!("Sum: {}", x + y); // no return value, returns ()
}
 
// Statements don't return values; expressions do
let y = {
    let x = 3;
    x + 1 // expression — note the missing semicolon
};

Control Flow

// if is an expression — can be used in assignments
let condition = true;
let number = if condition { 5 } else { 6 };
 
// loop — infinite loop, can return a value with break
let mut counter = 0;
let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2; // the loop expression evaluates to 20
    }
};
 
// Loop labels — specify break/continue targets in nested loops
'outer: for i in 0..5 {
    for j in 0..5 {
        if i == 2 && j == 2 {
            break 'outer;
        }
    }
}
 
// while
let mut n = 3;
while n != 0 {
    println!("{n}!");
    n -= 1;
}
 
// for — the most common loop in Rust
let arr = [10, 20, 30, 40, 50];
for element in arr.iter() {
    println!("Value: {element}");
}
for number in (1..4).rev() { // Range: 3, 2, 1
    println!("{number}!");
}

The Ownership System (In Depth)

The core purpose of the ownership system is managing data on the heap. Stack data, with its fixed size and predictable lifetime, is managed automatically by the compiler. Heap data requires a clear owner to determine when to free it.

Detailed rules of move semantics:

// String memory layout: stores (ptr, len, capacity) on the stack,
// actual data on the heap
let s1 = String::from("hello");
let s2 = s1; // shallow-copies the stack's (ptr, len, capacity), then invalidates s1
// s1 has been moved — can no longer be used
 
// clone — deep copy
let s3 = String::from("world");
let s4 = s3.clone();
println!("{s3}, {s4}"); // both are valid
 
// Function arguments also cause moves
let s = String::from("hello");
takes_ownership(s);
// println!("{s}"); // compile error: s's value has been moved
 
let x = 5;
makes_copy(x);
println!("{x}"); // fine: i32 implements Copy
 
// Return values also transfer ownership
let s1 = gives_ownership(); // return value moves into s1
 
fn takes_ownership(some_string: String) {
    println!("{some_string}");
} // some_string goes out of scope, drop is called
 
fn makes_copy(some_integer: i32) {
    println!("{some_integer}");
}
 
fn gives_ownership() -> String {
    String::from("yours")
}

Types that implement Copy: All integer types, f32/f64, bool, char, tuples (only when all elements are Copy — e.g., (i32, i32) is Copy, but (i32, String) is not).

References & Borrowing (In Depth)

// Immutable borrows — can have multiple
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // multiple immutable borrows coexist, no problem
 
// Mutable borrows — only one at a time
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
// let r2 = &mut s; // compile error: cannot have two mutable borrows simultaneously
println!("{r1}");
 
// Immutable and mutable cannot coexist
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // last use of r1 and r2 is here
let r3 = &mut s; // fine: r1 and r2 are no longer used
println!("{r3}");

A reference's scope starts from its introduction and ends at its last use (Non-Lexical Lifetimes, NLL). This means even if a reference hasn't syntactically left its curly braces, once the compiler confirms it's no longer used, it's considered "done."

The Slice Type

A slice is a reference to a contiguous sequence of elements in a collection:

let s = String::from("hello world");
 
// String slices
let hello = &s[0..5];   // "hello"
let world = &s[6..11];  // "world"
let whole = &s[..];     // "hello world"
let from_start = &s[..5];  // "hello"
let to_end = &s[6..];     // "world"
 
// A practical function: return the first word
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

Note the function signature uses &str instead of &String&str is a string slice that accepts both &String (via Deref coercion) and &str, making it more general.

Structs

// Regular struct
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}
 
// Creating an instance
let mut user1 = User {
    active: true,
    username: String::from("alice"),
    email: String::from("alice@example.com"),
    sign_in_count: 1,
};
 
// Struct update syntax — borrow values from another instance
let user2 = User {
    email: String::from("bob@example.com"),
    ..user1 // remaining fields moved from user1 (user1's String fields are no longer usable)
};
 
// Tuple struct — a named tuple
struct Color(i32, i32, i32);
let black = Color(0, 0, 0);
 
// Unit struct — no fields at all
struct AlwaysEqual;
 
// Methods
impl User {
    // &self borrows the instance
    fn summary(&self) -> String {
        format!("{} ({})", self.username, self.email)
    }
 
    // Associated function — doesn't take self, like a "constructor"
    fn new(username: String, email: String) -> User {
        User {
            active: true,
            username,
            email,
            sign_in_count: 0,
        }
    }
}

Enums & Pattern Matching (In Depth)

enum WebEvent {
    PageLoad,
    KeyPress(char),
    Paste(String),
    Click { x: i64, y: i64 },
}
 
fn inspect(event: WebEvent) {
    match event {
        WebEvent::PageLoad => println!("Page loaded"),
        WebEvent::KeyPress(c) => println!("Key pressed: '{c}'"),
        WebEvent::Paste(s) => println!("Pasted: \"{s}\""),
        WebEvent::Click { x, y } => println!("Clicked at: ({x}, {y})"),
    }
}
 
// if let — when you only care about one variant
let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is {max}");
}
 
// let else — introduced in Rust 1.65+
fn get_count(item: &Item) -> usize {
    let Item::Count(count) = item else {
        return 0;
    };
    count
}

Error Handling (Result / Option In Depth)

use std::fs::File;
use std::io::{self, Read};
 
// Result<T, E> definition
// enum Result<T, E> {
//     Ok(T),
//     Err(E),
// }
 
// Basic usage
let f = File::open("hello.txt");
let f = match f {
    Ok(file) => file,
    Err(error) => match error.kind() {
        io::ErrorKind::NotFound => {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Failed to create file: {error:?}");
            })
        }
        other_error => {
            panic!("Failed to open file: {other_error:?}");
        }
    },
};
 
// unwrap and expect — quick handling for prototyping
// let f = File::open("hello.txt").unwrap(); // panics on failure
// let f = File::open("hello.txt").expect("Could not open hello.txt"); // custom message
 
// Chaining with the ? operator
fn read_file_contents(path: &str) -> Result<String, io::Error> {
    let mut contents = String::new();
    File::open(path)?.read_to_string(&mut contents)?;
    Ok(contents)
}
 
// Common Option methods
let some_value: Option<i32> = Some(42);
some_value.unwrap();          // 42 — panics on None
some_value.unwrap_or(0);     // 42 — returns 0 on None
some_value.unwrap_or_default(); // 42 — returns default on None
some_value.expect("must have a value"); // 42 — panics with message on None
some_value.map(|x| x * 2);   // Some(84)
some_value.and_then(|x| Some(x + 1)); // Some(43)
some_value.filter(|&x| x > 40); // Some(42)
some_value.ok_or("no value");   // Ok(42) — converts to Result

Layer 2: Intermediate Usage

Generics

// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in &list[1..] {
        if item > largest {
            largest = item;
        }
    }
    largest
}
 
// Generic struct
struct Point<T> {
    x: T,
    y: T,
}
 
// Generics with different type parameters
struct Point2D<T, U> {
    x: T,
    y: U,
}
 
// Implementing methods on generic types
impl<T: std::fmt::Display> Point<T> {
    fn to_string(&self) -> String {
        format!("({}, {})", self.x, self.y)
    }
}
 
// Implementing methods for a concrete type
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

Traits & Trait Bounds (In Depth)

use std::fmt::{Display, Debug};
 
// Defining a trait
pub trait Describe {
    fn describe_author(&self) -> String;
 
    fn describe(&self) -> String {
        format!("Created by {}", self.describe_author())
    }
}
 
// Implementing a trait
struct Article {
    author: String,
    title: String,
}
 
impl Describe for Article {
    fn describe_author(&self) -> String {
        self.author.clone()
    }
}
 
// Trait as a parameter
fn print_description(item: &impl Describe) {
    println!("{}", item.describe());
}
 
// Equivalent trait bound syntax
fn print_description_bound<T: Describe>(item: &T) {
    println!("{}", item.describe());
}
 
// Multiple trait bounds
fn print_debug_and_display(item: &(impl Debug + Display)) {
    println!("Debug: {:?}", item);
    println!("Display: {}", item);
}
 
// where clause
fn compare_and_print<T, U>(t: &T, u: &U)
where
    T: Display + PartialOrd,
    U: Display,
{
    println!("t = {t}, u = {u}");
}
 
// Blanket implementation — the standard library example:
// impl<T: Display> ToString for T { ... }
let s = 42.to_string(); // i32 implements Display, so it gets to_string

Lifetimes (In Depth)

// Lifetime annotation syntax: 'a is a lifetime parameter
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
 
// Lifetimes in structs
struct ImportantExcerpt<'a> {
    part: &'a str,
}
 
fn main() {
    let novel = String::from("Once upon a time. There was a temple...");
    let first_sentence = novel.split('.').next().expect("Could not find '.'");
    let excerpt = ImportantExcerpt {
        part: first_sentence,
    };
}
 
// Lifetime elision rules (three rules the compiler uses to infer lifetimes)
// 1. Each reference parameter gets its own lifetime parameter
// 2. If there's exactly one input lifetime, it's assigned to all outputs
// 3. If there are multiple inputs but one is &self or &mut self, self's
//    lifetime is assigned to outputs
 
// The static lifetime 'static — valid for the entire program duration
let s: &'static str = "I am a static string";
 
// Lifetimes combined with generics
fn longest_with_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement: {ann}");
    if x.len() > y.len() { x } else { y }
}

Smart Pointers

use std::boxed::Box;
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;
use std::sync::Mutex;
 
// Box<T> — heap allocation, single owner
let b = Box::new(5);
println!("b = {b}");
 
// Typical use for Box: recursive types
enum List {
    Cons(i32, Box<List>),
    Nil,
}
use List::{Cons, Nil};
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
 
// Rc<T> — reference counting, multiple owners (single-threaded only)
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a); // increments reference count, no deep copy
let c = Rc::clone(&a);
println!("Reference count: {}", Rc::strong_count(&a)); // 3
 
// RefCell<T> — interior mutability, enforces borrowing rules at runtime
let value = Rc::new(RefCell::new(5));
let a = Rc::clone(&value);
*borrow_mut = 6;
println!("value = {value:?}");
 
// Arc<T> — atomic reference counting, thread-safe multiple ownership
let data = Arc::new(Mutex::new(vec![1, 2, 3]));

Concurrency

use std::thread;
use std::sync::{mpsc, Arc, Mutex};
use std::time::Duration;
 
// Spawning threads
let handle = thread::spawn(|| {
    for i in 1..10 {
        println!("Spawned thread number: {i}");
        thread::sleep(Duration::from_millis(1));
    }
});
 
for i in 1..5 {
    println!("Main thread number: {i}");
    thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
 
// Using move closures to capture ownership
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
    println!("Vector: {v:?}");
});
 
// Message passing (Channels)
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
    let vals = vec![
        String::from("Hello"),
        String::from("from"),
        String::from("the spawned thread"),
    ];
    for val in vals {
        tx.send(val).unwrap();
    }
});
for received in rx {
    println!("Received: {received}");
}
 
// Shared-state concurrency — Mutex + Arc
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
 
for _ in 0..10 {
    let counter = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        let mut num = counter.lock().unwrap();
        *num += 1;
    });
    handles.push(handle);
}
for handle in handles {
    handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap()); // 10
 
// Send and Sync traits
// Send — types can transfer ownership across threads (most types auto-implement)
// Sync — types can share references across threads (&T is safe to pass)
// Rc<T> is neither Send nor Sync, so it can't cross thread boundaries
// Arc<T> is both Send + Sync

Closures & Iterators

// Closures — anonymous functions that can capture environment variables
let plus_one = |x: i32| x + 1;
println!("{}", plus_one(5)); // 6
 
// Capturing environment variables
let x = 4;
let equal_to_x = |z| z == x; // immutably borrows x
println!("{}", equal_to_x(4)); // true
 
// move closure — forces taking ownership
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x; // x's ownership is moved into the closure
 
// Iterators
let v = vec![1, 2, 3, 4, 5];
 
// Lazy evaluation — iterator adapters don't execute immediately
let v2: Vec<i32> = v.iter()
    .map(|x| x + 1)         // each element +1
    .filter(|x| *x > 3)     // filter for values > 3
    .collect();              // consume the iterator, collect results
 
// Common iterator methods
let sum: i32 = v.iter().sum();                     // sum
let has_even = v.iter().any(|&x| x % 2 == 0);      // any even?
let all_positive = v.iter().all(|&x| x > 0);        // all positive?
let first_even = v.iter().find(|&&x| x % 2 == 0);   // find first even
 
// Custom iterator
struct Counter {
    count: u32,
}
 
impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}
 
impl Iterator for Counter {
    type Item = u32;
 
    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

Macros

// Declarative macros — defined with pattern matching
macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
    ($name:expr) => {
        println!("Hello, {}!", $name);
    };
}
 
say_hello!();           // Hello!
say_hello!("Rust");     // Hello, Rust!
 
// Simplified vec! macro implementation
macro_rules! my_vec {
    ($($x:expr),*) => {
        {
            let mut temp_vec = Vec::new();
            $(temp_vec.push($x);)*
            temp_vec
        }
    };
}
 
let v = my_vec![1, 2, 3];

The Module System

// Module declaration
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
        fn seat_at_table() {} // private (default)
    }
 
    mod serving {
        fn take_order() {}
        fn serve_order() {}
        fn take_payment() {}
    }
}
 
// Using use to bring paths into scope
use crate::front_of_house::hosting; // absolute path
use self::front_of_house::hosting;  // relative path
 
// Renaming with use as
use std::fmt::Result as FmtResult;
use std::io::Result as IoResult;
 
// Re-exporting with pub use
pub use crate::front_of_house::hosting;
 
// Splitting modules into separate files
// src/lib.rs:
// mod front_of_house; // tells the compiler to look for src/front_of_house.rs

Layer 3: Deep Analysis

How the Ownership System Guarantees Memory Safety

Rust's ownership system delivers compile-time memory safety guarantees through these core mechanisms:

  1. Ownership Transfer (Move): When a value is assigned to a new variable or passed into a function, ownership transfers. The original variable is invalidated, and the compiler ensures you never use a moved value. This eliminates double-free bugs.

  2. Borrowing Rules: Immutable borrows &T allow multiple readers. Mutable borrows &mut T allow a single writer. The two are mutually exclusive. This rule eliminates data races at compile time — no runtime locks or atomics needed for detection; the compiler simply rejects code with potential data races.

  3. Automatic Deallocation (Drop): When an owner leaves scope, Rust automatically calls Drop::drop(). This is similar to C++'s RAII (Resource Acquisition Is Initialization) pattern, but Rust's compiler guarantees each value is dropped exactly once.

  4. Lifetime Checking: The compiler uses lifetime annotations (mostly inferred) to ensure references never outlive the data they point to. This eliminates dangling pointers and use-after-free errors.

All of these checks happen at compile time with zero runtime overhead. The compiled code runs as efficiently as hand-written C — sometimes even more efficiently, because the compiler has more static information to optimize with.

Zero-Cost Abstractions

"Zero-cost abstractions" is one of Rust's core design principles, originating from C++: you don't pay for what you don't use. More specifically, Rust's high-level abstractions (generics, traits, iterators, closures, async/await) compile down to code that is as efficient as hand-written low-level code.

// Iterator zero-cost abstraction example
fn sum_iterator(data: &[i32]) -> i32 {
    data.iter().sum()
}
 
fn sum_manual(data: &[i32]) -> i32 {
    let mut total = 0;
    for &item in data {
        total += item;
    }
    total
}
// After compilation, the machine code for both is nearly identical

Generics achieve zero cost through monomorphization: the compiler generates a specialized copy of the code for each concrete type at compile time, rather than using type erasure and virtual dispatch at runtime like Java does.

Inside the Borrow Checker

The borrow checker is one of the most complex components in the Rust compiler. It operates based on Non-Lexical Lifetimes (NLL):

  1. Lifetime Parameterization: The compiler assigns a lifetime parameter to each reference, representing the region of code where that reference is valid.
  2. Constraint Solving: The compiler collects all constraints (e.g., "the return value's lifetime must be at least as long as the input reference") and solves them.
  3. Borrow Checking: The compiler verifies that at every program point, reference usage satisfies the borrowing rules — no simultaneous mutable and immutable borrows, no dangling references.
  4. NLL Optimization: A reference's lifetime doesn't simply extend to the end of its scope; it extends only to its last use point. This makes many previously uncompilable patterns valid.

async/await and Async Runtimes

Rust's async model differs from other languages: the language itself provides only async/await syntax and the Future trait — no runtime included. You choose an async runtime (such as Tokio):

use tokio::time::{sleep, Duration};
 
async fn fetch_data(id: u32) -> String {
    sleep(Duration::from_millis(100)).await;
    format!("Data {id}")
}
 
async fn process() {
    // Execute multiple futures concurrently
    let (a, b) = tokio::join!(
        fetch_data(1),
        fetch_data(2),
    );
    println!("Results: {a}, {b}");
}
 
#[tokio::main]
async fn main() {
    process().await;
}

A Future is a lazy state machine: it doesn't execute automatically — it must be .awaited or polled by a runtime. This design makes async code's memory overhead predictable (each future's size is determined at compile time), without needing dynamic stack allocation like Go goroutines.

unsafe Rust and FFI

When Rust's safety guarantees are too restrictive, you can use an unsafe block to perform five specific operations:

  1. Dereference raw pointers
  2. Call unsafe functions or methods
  3. Access or modify mutable static variables
  4. Implement unsafe traits
  5. Access union fields
unsafe fn dangerous() {}
 
unsafe {
    dangerous();
}
 
// FFI — calling C functions
extern "C" {
    fn abs(input: i32) -> i32;
}
 
fn main() {
    let x = unsafe { abs(-5) };
    println!("{x}");
}

unsafe is not a switch that "turns off safety checks" — it's shifting the responsibility for safety from the compiler to the programmer. unsafe blocks should be as small as possible and wrapped behind safe APIs.

Comparison with Other Systems Languages

FeatureRustC++GoZig
Memory safetyCompile-time guaranteedManual / smart pointersGCManual
Concurrency safetyCompile-time checkedManual synchronizationCSP modelManual
Runtime overheadNearly zeroZero (optional RTTI/exceptions)GC overheadMinimal
Generics/polymorphismGenerics + TraitsTemplates + virtual functionsGenerics (limited)Compile-time generics
Error handlingResult / OptionExceptions / error codeserror interfaceError unions
Build systemCargo (built-in)CMake/Make etc.go build (built-in)zig build (built-in)

4. Key Notes (Cornell Note-taking System)

Cornell Notes: Keywords/cues on the left, detailed notes on the right, summary at the bottom.

Quick Reference Table

Cue / KeywordDetailed Notes
Ownership RulesOne owner per value; only one owner at a time; value is dropped when owner leaves scope. Original variable invalidated after move.
Borrowing Rules&T immutable references can be multiple; &mut T mutable reference is exclusive; the two cannot coexist. Compile-time checked, zero runtime cost.
LifetimesDescribe the scope for which a reference is valid. Mostly auto-inferred (elision rules). Explicit annotation needed when structs hold references. 'static means valid for the entire program.
Copy vs CloneCopy: implicit shallow copy (simple stack types). Clone: explicit deep copy (clone() method). Types implementing Drop cannot be Copy.
Option<T>Some(T) or None. Replaces null. Compiler forces handling of None. Common methods: unwrap, unwrap_or, map, and_then, ok_or.
Result<T, E>Ok(T) or Err(E). ? operator simplifies error propagation. unwrap/expect for prototyping.
Pattern Matchingmatch exhausts all variants; if let handles a single variant; let else provides a fallback.
TraitsDefine shared behavior. Default implementations, trait bounds (impl Trait/where), blanket implementations. Orphan rule prevents implementation conflicts.
GenericsParameterized types. Compile-time monomorphization — generates specialized code for each concrete type, zero runtime cost.
Smart PointersBox<T> heap allocation; Rc<T> single-threaded reference counting; Arc<T> thread-safe reference counting; RefCell<T> runtime borrow checking.
ConcurrencyThreads (std::thread), channels (mpsc), Mutex + Arc. Send + Sync traits guarantee thread safety.
ClosuresAnonymous functions that capture environment variables. Fn (immutable borrow), FnMut (mutable borrow), FnOnce (takes ownership).
IteratorsLazy evaluation. Iterator trait requires only next(). Adapters (map/filter) and consumers (collect/sum) compose.

Core API / Trait Reference

Type / TraitPurposeExample
StringGrowable UTF-8 stringString::from("hello"), s.push_str(" world")
Vec<T>Growable arrayvec![1, 2, 3], v.push(4), v.iter()
HashMap<K, V>Key-value mappingHashMap::new(), map.insert(k, v), map.get(&k)
Option<T>Potentially absent valueSome(v), None, .unwrap_or(default)
Result<T, E>Recoverable errorOk(v), Err(e), ? operator
Box<T>Heap-allocated pointerBox::new(value), recursive types
Rc<T>Single-threaded reference countingRc::new(v), Rc::clone(&rc)
Arc<T>Thread-safe reference countingArc::new(v), Arc::clone(&arc)
RefCell<T>Runtime borrow checkingRefCell::new(v), .borrow(), .borrow_mut()
Mutex<T>Mutual exclusion lockMutex::new(v), .lock().unwrap()
IteratorIterator trait.next(), .map(), .filter(), .collect()
DisplayFormat output {}impl fmt::Display for T
DebugDebug output {:?}#[derive(Debug)]
CloneExplicit deep copy.clone()
CopyImplicit copyIntegers, floats, booleans, char
DropCustom cleanupimpl Drop for T
SendSafe to transfer across threadsMost types auto-implement
SyncSafe to share across threadsArc<T> requires T: Send + Sync
From/IntoType conversionimpl From<T> for U, .into()
Read/WriteI/O traitsstd::io::Read, std::io::Write

Section Summary

Rust's knowledge system revolves around the ownership system. Understanding ownership, borrowing, and lifetimes means grasping Rust's most core and most unique aspects. The trait system provides flexible polymorphism and code reuse. Option/Result delivers type-driven error handling. Generics and iterators bring zero-cost high-level abstractions. Concurrency safety guarantees derive directly from the ownership system's borrowing rules. All these features work together to fulfill Rust's promise of "eliminating bugs at compile time."

5. Review & Practice (SQ3R · Recite & Review)

SQ3R final two steps: Recite key points, reinforce understanding through practice and review.

Key Takeaways

  1. Ownership is Rust's soul — one owner per value, original variable invalidated after move, automatic drop when scope ends.
  2. Borrowing rules eliminate data races at compile time: multiple immutable references or one mutable reference — never both.
  3. Lifetimes ensure references never outlive the data they point to, eliminating dangling pointers.
  4. Option<T> replaces null, Result<T, E> replaces exceptions — the compiler forces you to handle error cases.
  5. Traits define shared behavior with support for default implementations, trait bounds, and blanket implementations.
  6. Zero-cost abstractions mean high-level features (iterators, generics, closures) compile to code as efficient as hand-written low-level code.
  7. Send + Sync make the compiler verify thread safety at compile time — "fearless concurrency" is not a slogan, it's compiler-enforced.

Hands-On Exercises

  1. Basic: Write a function that takes a string slice and returns the longest word in it. Return None if the string is empty.
  2. Ownership: Implement a simple StringBuilder using String methods for append, insert, and clear operations, paying attention to ownership transfers.
  3. Traits: Define a Drawable trait, implement it for Circle and Rectangle, then write a function that accepts any Drawable type and calls its methods.
  4. Error Handling: Use the ? operator to chain multiple fallible file operations. Define a custom error type and implement the From trait for error conversion.
  5. Concurrency: Use Arc<Mutex<T>> to implement a simple thread-safe counter. Spawn 10 threads, each incrementing 1000 times, and verify the final result is 10000.
  6. Iterators: Implement a custom Fibonacci iterator that generates the Fibonacci sequence. Use .take(n).collect() to get the first n numbers.

Common Pitfalls

  1. Creating new Strings in a loop without reuse — each iteration allocates and frees heap memory. Consider using String::with_capacity() or creating the String outside the loop and calling clear() to reuse it.
  2. Overusing clone()clone() deep-copies data. If you're cloning frequently, it may indicate your ownership design needs rethinking. Prefer references.
  3. Confusing String and &strString is an owned, heap-allocated string; &str is a string slice (borrowed). Prefer &str for function parameters — it's more general.
  4. Ignoring Result errors — when using .unwrap(), be clear about whether the call can actually fail. Use ? to propagate errors in library code; only use unwrap/expect in application code.
  5. Lifetime annotation panic — when the compiler asks for lifetime annotations, don't reflexively add 'static. Think about the actual relationship between references — in most cases, 'a is all you need.
  6. Runtime borrow conflicts with RefCellRefCell defers borrow checking to runtime. If you call borrow_mut() while an immutable borrow (borrow()) is active, the program will panic.

Further Reading