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.