I put this guide together to explain each Rust concept demonstrated in the example programs, with comparisons to C and Java. The more I dug into Rust’s design decisions, the more I appreciated how much thought went into making safety guarantees that don’t cost you performance. Here’s what I found.

Supporting Code Examples


1. OWNERSHIP SYSTEM

What it is: Rust’s core memory management system. Every value has a single owner, and when the owner goes out of scope, the value is dropped (freed). This was the concept that took the longest to click for me, but once it did, everything else fell into place.

Example from code:

let task = Task::new(next_id, description.trim().to_string());
tasks.push(task); // task is MOVED into vector - can't use 'task' anymore

Comparison:

  • C: Manual memory management with malloc()/free(). You control everything but can cause memory leaks, double frees, use-after-free bugs.
  • Java: Garbage collector manages memory automatically. Objects can have multiple references. GC runs periodically causing unpredictable pauses.
  • Rust: Compile-time memory safety. No GC overhead. No manual free(). Compiler enforces single ownership, prevents entire classes of bugs at compile time.

Key difference: Rust catches memory errors at compile time, not runtime. No GC pauses like Java, no manual tracking like C.


2. BORROWING & REFERENCES

What it is: Temporarily access data without taking ownership. Immutable borrows (&T) allow reading. Mutable borrows (&mut T) allow modification. The rules here are strict, but they’re what make Rust’s concurrency guarantees possible.

Rules:

  • Any number of immutable borrows OR exactly one mutable borrow
  • References must always be valid (no dangling pointers)

Example from code:

for task in &tasks {        // Immutable borrow - can read, can't modify
    task.display();
}

tasks.iter_mut().find(...)  // Mutable borrow - can modify

Comparison:

  • C: Pointers can be copied freely. No compile-time checks for dangling pointers or data races. const is advisory only.
  • Java: All object variables are references. Can have unlimited references. Synchronisation needed for thread safety (manual).
  • Rust: Borrow checker enforces rules at compile time. Impossible to have data races. & vs &mut distinction is enforced.

Key difference: Rust prevents data races at compile time through borrow rules. C has no protection. Java requires manual synchronisation.


3. STRUCTS & IMPLEMENTATIONS

What it is: Custom data types (structs) with associated methods (impl blocks). Rust separates data from behaviour, which I found surprisingly clean once I got used to it.

Example from code:

struct Task {
    id: u32,
    description: String,
    completed: bool,
}

impl Task {
    fn new(id: u32, description: String) -> Self { ... }
    fn display(&self) { ... }
    fn toggle_complete(&mut self) { ... }
}

Comparison:

  • C: struct for data only. Functions are separate. No methods, no encapsulation. Use function pointers for polymorphism.
  • Java: Classes combine data and methods. Inheritance-based. Methods inside class definition. this is implicit.
  • Rust: Structs for data, impl blocks for methods. Composition over inheritance. self is explicit (&self, &mut self, self).

Key difference: Rust’s explicit self parameter shows ownership semantics. Java’s this is always a reference. C has no concept of methods.


4. ENUMS & PATTERN MATCHING

What it is: Enums can hold data (algebraic data types). Pattern matching with match is exhaustive, the compiler ensures all cases are handled. This is one of the features that really sold me on Rust; coming from C where switch statements silently fall through, exhaustive matching feels like a revelation.

Example from code:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

match msg {
    Message::Quit => println!("Quitting..."),
    Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
    // Must handle ALL variants or use _ catch-all
}

Comparison:

  • C: Enums are just integers. No data attached. Switch statements don’t require exhaustiveness. Easy to forget cases.
  • Java: Enums can have fields/methods but syntax is verbose. Switch (pre-Java 14) doesn’t enforce exhaustiveness. Pattern matching added in Java 21.
  • Rust: Enums are powerful sum types. Each variant can hold different data. match is exhaustive, compiler error if you miss a case.

Key difference: Rust enums are true algebraic data types. C enums are just named integers. Java enums are closer but less ergonomic.


5. OPTION & RESULT TYPES

What it is: Rust has no null or exceptions. Option<T> represents optional values. Result<T, E> represents success or error. What I found interesting when I started using these is how they make impossible states unrepresentable, you simply can’t forget to handle the “not found” case.

Example from code:

// Option<T> - either Some(value) or None
match tasks.iter_mut().find(|t| t.id == id) {
    Some(task) => task.toggle_complete(),
    None => println!("Task not found!"),
}

// Result<T, E> - either Ok(value) or Err(error)
fn divide(a: f64, b: f64) -> Result<f64, MathError> {
    if b == 0.0 {
        Err(MathError::DivisionByZero)
    } else {
        Ok(a / b)
    }
}

Comparison:

  • C: Return codes (integers) or NULL pointers. Easy to ignore errors. No type safety for error handling.
  • Java: null everywhere (NullPointerException). Checked exceptions force handling but are verbose. Unchecked exceptions can be ignored.
  • Rust: No null! Option<T> makes absence explicit. Result<T, E> makes errors part of the type signature. Compiler forces you to handle both cases.

Key difference: Rust eliminates null pointer errors at compile time. C has no protection. Java has runtime NullPointerExceptions.


6. TRAITS

What it is: Define shared behaviour across types. Similar to interfaces but more powerful, they can have default implementations. The flexibility here is remarkable; you can implement traits for types you don’t even own.

Example from code:

trait Summary {
    fn summarize(&self) -> String;
    
    fn announce(&self) {  // Default implementation
        println!("Breaking news: {}", self.summarize());
    }
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{} ({})", self.headline, self.location)
    }
}

Comparison:

  • C: No built-in concept. Use function pointers in structs (vtables) manually. No type safety.
  • Java: Interfaces define contracts. Can have default methods (Java 8+). Tied to inheritance hierarchy.
  • Rust: Traits can be implemented for any type, even types you don’t own. More flexible than Java interfaces. Trait bounds enable generic programming.

Key difference: Rust traits can be implemented for existing types (even from other libraries). Java interfaces require control over the class. C has no equivalent.


7. GENERICS

What it is: Write code that works with multiple types. Monomorphisation means zero runtime cost, the compiler generates specialised code for each type. This is one of those areas where Rust genuinely delivers on “zero-cost abstractions.”

Example from code:

struct Pair<T, U> {
    first: T,
    second: U,
}

fn notify<T: Summary>(item: &T) {  // T must implement Summary trait
    println!("Notification: {}", item.summarize());
}

Comparison:

  • C: No generics. Use void* and casting (type unsafe) or macros (no type checking).
  • Java: Generics use type erasure, type info removed at runtime. Boxing overhead for primitives. Runtime type checks.
  • Rust: Monomorphisation, generates specialised code for each type. Zero runtime cost. Full type information at compile time.

Key difference: Rust generics have zero runtime overhead. Java generics use type erasure. C has no real generics.


8. LIFETIMES

What it is: Explicit annotations showing how long references are valid. Prevents dangling references at compile time. Lifetimes were the concept I found hardest to grasp initially, but the key insight is that they’re not creating new constraints, they’re making existing constraints visible to the compiler.

Example from code:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// 'a means: returned reference lives as long as both inputs

Comparison:

  • C: No lifetime tracking. Dangling pointers are common bugs. Returning pointer to local variable compiles but crashes at runtime.
  • Java: GC handles lifetimes. Objects live until no references exist. Can’t have dangling references but GC overhead.
  • Rust: Lifetime annotations let compiler verify references are always valid. Catches dangling reference bugs at compile time.

Key difference: Rust prevents dangling references at compile time. C allows them (undefined behaviour). Java prevents them with GC overhead.


9. CLOSURES

What it is: Anonymous functions that can capture variables from their environment. Three capture modes: borrow, mutable borrow, or move (take ownership). The explicit control over capture semantics is something I really appreciate coming from other languages.

Example from code:

let x = 10;
let add_x = |y| x + y;  // Borrows x

let consume = move || println!("x: {}", x);  // Takes ownership of x

numbers.iter().map(|n| n * 2).collect()  // Closure as argument

Comparison:

  • C: No closures. Function pointers can’t capture environment. Must pass context manually via void*.
  • Java: Lambdas (Java 8+) can capture variables but they must be “effectively final”. Always captured by reference.
  • Rust: Explicit control over capture mode (move keyword). Compiler infers the most restrictive mode. Can capture by value or reference.

Key difference: Rust gives explicit control over capture semantics. Java captures are always references to effectively final variables. C has no closures.


10. SMART POINTERS

What it is: Types that act like pointers but have additional metadata and capabilities. What I find elegant about Rust’s approach is that ownership semantics are encoded directly in the type system.

Examples from code:

Box<T>        // Heap allocation, single owner
Arc<T>        // Atomic reference counting - shared ownership across threads
Mutex<T>      // Mutual exclusion - safe mutable access
RefCell<T>    // Runtime borrow checking (not shown but common)

Comparison:

  • C: Raw pointers only. Manual malloc/free. Reference counting must be implemented manually. No thread safety guarantees.
  • Java: All objects are heap-allocated references. GC manages everything. synchronized for thread safety.
  • Rust: Multiple smart pointer types for different use cases. Box for simple heap allocation. Arc<Mutex<T>> for thread-safe shared mutable state. Compile-time guarantees where possible.

Key difference: Rust smart pointers encode ownership semantics in the type system. C has no such abstractions. Java hides everything behind GC.


11. THREADING & CONCURRENCY

What it is: Rust’s ownership system prevents data races at compile time. “Fearless concurrency”, if it compiles, it’s thread-safe. This is the feature that genuinely impressed me the most when I started digging into Rust.

Example from code:

let counter = Arc::new(Mutex::new(0));  // Shared mutable state
let counter_clone = Arc::clone(&counter);

thread::spawn(move || {
    let mut num = counter_clone.lock().unwrap();
    *num += 1;
});

Comparison:

  • C: Pthreads or OS threads. Manual synchronisation with mutexes. Easy to create data races, deadlocks. No compile-time checks.
  • Java: Built-in threading. synchronized keyword. Easy to forget synchronisation. Data races possible. Deadlocks possible.
  • Rust: Ownership + type system prevents data races at compile time. Send and Sync traits mark thread-safe types. Can’t compile code with data races.

Key difference: Rust catches data races at compile time. C and Java allow them, only caught at runtime (if at all).


12. ITERATORS

What it is: Lazy evaluation of sequences. Iterator adapters can be chained. Only evaluated when consumed (e.g., collect()). The research on Rust’s iterator performance is compelling, they genuinely compile down to the same assembly as hand-written loops.

Example from code:

let result: Vec<i32> = numbers
    .iter()
    .filter(|&&x| x % 2 == 0)  // Lazy - not executed yet
    .map(|&x| x * x)            // Lazy - not executed yet
    .collect();                 // Now everything executes

Comparison:

  • C: No iterators. Manual loops with indices or pointers. No abstraction for iteration patterns.
  • Java: Streams API (Java 8+) similar concept. Can be lazy. Less ergonomic syntax. Boxing overhead for primitives.
  • Rust: Zero-cost abstractions. Iterator code compiles to same assembly as hand-written loops. Lazy evaluation. Composable.

Key difference: Rust iterators have zero runtime cost. Java streams have overhead. C has no iterator abstraction.


13. ERROR HANDLING PATTERNS

What it is: Rust uses Result<T, E> for recoverable errors and panic! for unrecoverable errors. No exceptions. Here’s what clicked for me: errors as values means the type system forces you to deal with them, which eliminates an entire class of “forgot to handle the error” bugs.

Patterns:

// Pattern 1: match
match divide(10.0, 0.0) {
    Ok(result) => println!("Result: {}", result),
    Err(e) => println!("Error: {}", e),
}

// Pattern 2: unwrap (panics on error - use for prototyping)
let result = divide(10.0, 2.0).unwrap();

// Pattern 3: ? operator (propagates error to caller)
fn do_math() -> Result<f64, MathError> {
    let x = divide(10.0, 2.0)?;  // Returns early if error
    let y = divide(x, 3.0)?;
    Ok(y)
}

Comparison:

  • C: Return codes (integers). Easy to ignore. No standard error type. Error handling often skipped.
  • Java: Exceptions (checked and unchecked). Can be caught anywhere up the call stack. Checked exceptions force handling but are verbose.
  • Rust: Result type makes errors explicit in function signature. ? operator makes propagation ergonomic. Compiler forces handling.

Key difference: Rust errors are values, not control flow. C errors are easy to ignore. Java exceptions can skip stack frames.


14. TRAIT OBJECTS & DYNAMIC DISPATCH

What it is: dyn Trait enables runtime polymorphism. Trades compile-time monomorphisation for runtime flexibility. The explicit choice between static and dynamic dispatch is something I wish more languages offered.

Example from code:

trait Animal {
    fn make_sound(&self) -> &str;
}

// Vec of different types implementing Animal
let animals: Vec<Box<dyn Animal>> = vec![
    Box::new(Dog),
    Box::new(Cat),
];

for animal in animals {
    println!("{}", animal.make_sound());  // Runtime dispatch
}

Comparison:

  • C: Function pointers in structs (manual vtables). No type safety. Verbose and error-prone.
  • Java: All interface/class methods are virtual by default. Runtime dispatch everywhere. Can’t opt out.
  • Rust: Static dispatch by default (monomorphisation). dyn Trait for dynamic dispatch when needed. Explicit choice.

Key difference: Rust defaults to static dispatch (zero cost). Java defaults to dynamic dispatch. C requires manual implementation.


15. MACROS

What it is: Code generation at compile time. Macros operate on syntax trees, not text (unlike C preprocessor). The hygiene guarantees here are a massive improvement over C’s preprocessor macros.

Example from code:

macro_rules! create_function {
    ($func_name:ident) => {
        fn $func_name() {
            println!("Function {:?} was called", stringify!($func_name));
        }
    };
}

create_function!(foo);  // Generates foo() function at compile time

Comparison:

  • C: Preprocessor macros are text substitution. No type safety. Can cause subtle bugs. No hygiene.
  • Java: No macros. Annotation processing is complex and limited. Reflection is runtime only.
  • Rust: Hygienic macros operate on AST. Type-safe. Compile-time code generation. More powerful than C, safer than C++.

Key difference: Rust macros are hygienic and type-safe. C macros are text substitution. Java has no equivalent.


16. STRING TYPES

What it is: Rust has two main string types with different ownership semantics. The String vs &str distinction confused me at first, but it maps directly to Rust’s ownership model, once that connection clicked, it made perfect sense.

Types:

String      // Owned, heap-allocated, growable (like Vec<u8>)
&str        // Borrowed string slice, immutable view into string data

let owned = String::from("hello");
let borrowed: &str = "world";  // String literal
let slice: &str = &owned[0..2]; // Slice of owned string

Comparison:

  • C: char* for everything. Null-terminated. No length tracking. Manual memory management. No UTF-8 guarantees.
  • Java: String is immutable. StringBuilder for mutable. All heap-allocated. GC managed. UTF-16 encoding.
  • Rust: String vs &str distinction makes ownership explicit. UTF-8 guaranteed. Length tracked. No null terminator needed.

Key difference: Rust distinguishes owned vs borrowed strings in the type system. C has no such distinction. Java strings are always owned references.


17. MODULES & VISIBILITY

What it is: Code organisation with explicit visibility control. Private by default, which is the right default from a security perspective.

Example from code:

mod advanced;  // Declares module from advanced.rs

pub fn run_advanced_demo() { ... }  // Public function
fn helper() { ... }                  // Private by default

Comparison:

  • C: Header files and source files. static for file-local. No namespace system. Global namespace pollution.
  • Java: Packages and classes. Public/private/protected. Public by default in same package.
  • Rust: Module system with explicit pub. Private by default. No header files. Clear visibility rules.

Key difference: Rust is private by default (secure by default). Java is package-public by default. C has weak encapsulation.


KEY TAKEAWAYS

Rust vs C:

  • Memory safety: Rust catches at compile time what C catches at runtime (or never)
  • No undefined behaviour: Rust eliminates entire classes of C bugs
  • Zero cost: Rust abstractions compile to same code as hand-written C
  • Modern features: Traits, generics, pattern matching that C lacks

Rust vs Java:

  • No GC: Rust has predictable performance, no pause times
  • Explicit ownership: Rust makes memory semantics clear in types
  • Zero cost abstractions: Rust generics have no runtime overhead
  • Systems programming: Rust can do low-level work Java can’t

Rust’s Philosophy:

  • Safety without garbage collection
  • Concurrency without data races
  • Abstraction without overhead
  • Catch bugs at compile time, not runtime

LEARNING PATH RECOMMENDATION

  1. Start with: Ownership, borrowing, structs (main.rs basics)
  2. Then learn: Enums, pattern matching, Option/Result
  3. Next: Traits, generics, lifetimes (advanced.rs)
  4. Finally: Concurrency, smart pointers, advanced patterns

The compiler is your teacher, read error messages carefully. They’re designed to teach you Rust’s rules, and honestly, they’re some of the best error messages I’ve encountered in any language.