Rust Practice (Basics)
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.