Skip to content

Rust Practice (Basics) - Wyatt's Notes

Rust — Interactive Practice

10 auto-graded practice problems covering core Rust concepts from ownership to concurrency. Select an answer, submit, and review the explanation.

Worked Examples

Example 1: Ownership and Move Semantics

fn main() \{
    // String is heap-allocated, ownership moves
    let s1 = String::from("hello");
    let s2 = s1;  // s1 is moved to s2
    // println!("\{\}", s1);  // ERROR: value used here after move
    println!("\{\}", s2);  // OK: s2 owns the string

    // Clone creates a deep copy
    let s3 = String::from("world");
    let s4 = s3.clone();  // s3 is not moved, both valid
    println!("s3=\{\}, s4=\{\}", s3, s4);

    // i32 implements Copy, so it's copied, not moved
    let x = 42;
    let y = x;  // x is copied, not moved
    println!("x=\{\}, y=\{\}", x, y);  // Both valid
\}

Output:

hello
s3=world, s4=world
x=42, y=42

Key insight: Types that implement Copy (like i32, f64, bool) are copied, not moved. Types like String and Vec that own heap data are moved to prevent double-free.


Example 2: Borrowing and References

// Immutable references: many allowed
fn calculate_length(s: &String) -> usize \{
    s.len()
\}

// Mutable reference: only one at a time
fn append_world(s: &mut String) \{
    s.push_str(" world");
\}

fn main() \{
    let mut s = String::from("hello");

    // Multiple immutable references are fine
    let len1 = calculate_length(&s);
    let len2 = calculate_length(&s);
    println!("Length: \{\}, \{\}", len1, len2);

    // One mutable reference
    append_world(&mut s);
    println!("Modified: \{\}", s);

    // Cannot have both immutable and mutable at the same time
    let r1 = &s;
    let r2 = &s;
    // let r3 = &mut s;  // ERROR: cannot borrow as mutable
    println!("r1=\{\}, r2=\{\}", r1, r2);
    // r1 and r2 are no longer used after this point
    let r3 = &mut s;  // OK: r1 and r2 are not used
    r3.push_str("!");
    println!("Final: \{\}", s);
\}

Output:

Length: 5, 5
Modified: hello world
r1=hello world, r2=hello world
Final: hello world!

Key insight: Rust’s borrow checker ensures you can have either many immutable references OR one mutable reference, never both simultaneously. This prevents data races at compile time.


Example 3: Option and Pattern Matching

fn find_user(id: u32) -> Option<String> \{
    match id \{
        1 => Some(String::from("Alice")),
        2 => Some(String::from("Bob")),
        _ => None,
    \}
\}

fn main() \{
    // Pattern matching with if let
    if let Some(name) = find_user(1) \{
        println!("Found user: \{\}", name);
    \}

    // match expression
    match find_user(42) \{
        Some(name) => println!("User: \{\}", name),
        None => println!("User not found"),
    \}

    // unwrap_or for default values
    let name = find_user(99).unwrap_or(String::from("Anonymous"));
    println!("Name: \{\}", name);

    // map and and_then combinators
    let greeting = find_user(1)
        .map(|name| format!("Hello, \{\}!", name))
        .unwrap_or(String::from("Hello, stranger!"));
    println!("\{\}", greeting);

    // The ? operator in functions returning Option
    fn get_first_char(s: &str) -> Option<char> \{
        let first = find_user(1)?;  // Returns None if find_user returns None
        Some(first.chars().next()?)
    \}

    match get_first_char("test") \{
        Some(c) => println!("First char: \{\}", c),
        None => println!("No character"),
    \}
\}

Output:

Found user: Alice
User not found
Name: Anonymous
Hello, Alice!
First char: A

Key insight: Option<T> replaces null with a type-safe enum. Use match, if let, map, and_then, and ? to handle optional values without panicking.


Example 4: Traits and Generics

use std::fmt::\{Display, Debug\};

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

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

// Implement trait for a struct
struct Article \{
    title: String,
    content: String,
\}

impl Summary for Article \{
    fn summarize(&self) -> String \{
        format!("\{\}: \{\}", self.title, self.content)
    \}
\}

// Generic function with trait bounds
fn notify(item: &impl Summary) \{
    println!("Breaking news: \{\}", item.summarize());
\}

// Multiple trait bounds
fn display_and_summarize(item: &(impl Summary + Display)) \{
    println!("Display: \{\}", item);
    println!("Summary: \{\}", item.summarize());
\}

fn main() \{
    let article = Article \{
        title: String::from("Rust Ownership"),
        content: String::from("Ownership prevents memory leaks..."),
    \};

    notify(&article);

    // Use the default implementation
    println!("Preview: \{\}", article.preview());
\}

Output:

Breaking news: Rust Ownership: Ownership prevents memory leaks...
Preview: Rust Ownership: Owner...

Key insight: Traits define shared behavior. Generic functions with trait bounds (impl Trait or T: Trait) work with any type that implements the required traits, enabling zero-cost polymorphism.


Ownership and Mutability


Lifetimes and Trait Bounds


Traits and Generics


Concurrency and Unsafe

Intuition

Rust combines low-level control with high-level ergonomics: Pattern matching, algebraic data types (Option, Result), and traits provide expressive abstractions without runtime overhead. Zero-cost abstractions mean you do not pay for features you do not use.

Why it matters: Rust’s memory safety guarantees make it ideal for systems programming, embedded devices, and performance-critical applications.

The key insight: Option<T> and Result<T, E> replace null and exceptions with explicit types that force you to handle absence and error cases.

Common Mistakes

Confusing &T with &mut T: &T is an immutable reference (many allowed). &mut T is a mutable reference (only one allowed at a time). This prevents data races at compile time. You cannot have both simultaneously.

Forgetting that match must be exhaustive: Rust requires match to handle all possible cases. A missing arm causes a compilation error. Use _ as a catch-all when you dont need to handle all cases explicitly.

Not using Result for error handling: Rust prefers Result<T, E> over panics for recoverable errors. Functions that can fail return Result, and callers must handle the error with match, ?, or .unwrap(). Panicking should be reserved for truly unrecoverable situations.

Cross-References

  • Site Home: Main landing page for rust notes.
  • Practice: Practice problems for revision.