Quiz: Essential Rust Memory Management
(Borrow) check yo self before you wreck yo self! 🦀
Ready to test your Rust memory management skills? 🦀
This quiz will challenge your understanding of Rust’s ownership system, borrowing rules, lifetimes, and smart pointers.
Note: The questions are formatted in ~50-column width to ensure readability across all devices. (Suggestions for improvement are welcome!)
Whether you’re a seasoned Rustacean or just getting started with memory management, this quiz will help reinforce your knowledge. Let’s dive in! 🦀
What happens when you run this code? Try to predict the output or error:
fn main() { let philosopher = String::from("Zeno of Citium"); let greeting = philosopher;
println!("Hello, {}!", philosopher);}This code fails to compile because of Rust’s ownership rules. When we assign philosopher to greeting, the ownership of the String is moved to greeting. After this move, philosopher is no longer valid to use.
Here are three ways to fix this:
- Clone the string (creates a new copy):
let greeting = philosopher.clone();- Use a reference (borrows the value):
let greeting = &philosopher;- Use a string slice (borrows part of the string):
let greeting = &philosopher[..];Each solution has different use cases and performance implications. Cloning is more expensive but gives you ownership, while references are cheaper but have lifetime constraints.
What happens when you run this code? Think about ownership transfer:
fn take_knowledge(knowledge: String) { println!("Knowledge: {}", knowledge);}
fn main() { let wisdom = String::from("know thyself"); take_knowledge(wisdom); // What happens to our wisdom? println!("Do you {}", wisdom);}The code fails to compile because wisdom’s ownership moved to take_knowledge and therefore can’t be used afterward.
Here are three ways to fix this issue:
- Pass by reference (borrow the value):
fn borrow_it(text: &String) { println!("Inside: {}", text);}borrow_it(&wisdom); // Now wisdom can be used after- Clone the value (create a new copy):
take_knowledge(wisdom.clone()); // Original wisdom remains valid- Return ownership from the function:
fn take_and_return(text: String) -> String { println!("Inside: {}", text); text // Return ownership back}let wisdom = take_and_return(wisdom); // Reassign returned ownershipEach approach has different use cases:
- References: Most efficient, but need lifetime management
- Cloning: Simple but potentially expensive
- Returning ownership: Useful for transforming values
Best practice: Use references unless you need ownership transfer.
Will this code compile? If so, why? If not, what’s wrong?
fn first_word(s: &str) -> &str { // No explicit lifetimes? match s.find(' ') { Some(pos) => &s[0..pos], None => s, }}
fn main() { let name = String::from("Seneca the Younger"); let first = first_word(&name); println!("Hello, {}", first);}This code compiles successfully thanks to Rust’s lifetime elision rules. These rules allow the compiler to automatically infer lifetimes in common patterns.
The three lifetime elision rules are:
- Each parameter gets its own lifetime parameter
- If there’s exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
- If there are multiple input lifetime parameters, but one of them is &self or &mut self, the lifetime of self is assigned to all output lifetime parameters
This function is equivalent to:
fn first_word<'a>(s: &'a str) -> &'a str { // ... same implementation}Common patterns where elision works:
// These don't need explicit lifetimesfn get_str(s: &str) -> &str { s }fn get_first(s: &str) -> &str { &s[0..1] }
// These would need explicit lifetimesfn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}Best practice: Let elision work for you when possible, but understand when explicit lifetimes are needed.
What’s wrong with this recursive type definition?
#[derive(Debug)]enum CatList { Cons(i32, CatList), // Recursive without indirection Nil,}
fn main() { let catlist = CatList::Cons(1, CatList::Cons(2, CatList::Cons(3, CatList::Nil)));}This code fails because the compiler can’t determine the size of CatList at compile time. The recursive nature of the type means it could be infinitely large!
Here’s how to fix it using Box<T>:
#[derive(Debug)]enum CatList { Cons(i32, Box<CatList>), // Box provides a fixed-size pointer Nil,}
fn main() { let catlist = CatList::Cons(1, Box::new(CatList::Cons(2, Box::new(CatList::Cons(3, Box::new(CatList::Nil))))));}Why Box<T> works:
- Box provides a fixed-size pointer (usually 8 bytes on 64-bit systems)
- The actual data is stored on the heap
- The compiler now knows exactly how much space to allocate
Common use cases for Box<T>:
- Recursive data structures (linked lists, trees)
- Large data you want to ensure is heap-allocated
- Trait objects when you need dynamic dispatch
Best practice: Use Box<T> when you need:
- Recursive types
- To ensure heap allocation
- To move large data without copying
What will this code print? Count carefully!
use std::rc::Rc;
fn main() { let text = Rc::new(String::from("Meditations")); // Count: 1 let marcus = Rc::clone(&text); // What happens here? let aurelius = Rc::clone(&text); // And here? println!( "Reference count: {}", Rc::strong_count(&text) );}Let’s break down how Rc works:
- Initial creation with
Rc::new(): count = 1 - First clone for
marcus: count = 2 - Second clone for
aurelius: count = 3
Important Rc characteristics:
use std::rc::Rc;
fn demonstrate_rc() { let original = Rc::new(String::from("Shared")); println!("Count after creation: {}", Rc::strong_count(&original)); // 1
{ let copy = Rc::clone(&original); println!("Count inside scope: {}", Rc::strong_count(&original)); // 2 } // copy is dropped here
println!("Count after scope: {}", Rc::strong_count(&original)); // 1}Key points:
- Rc::clone() is cheap - it only increments a counter
- Rc is for single-threaded scenarios only
- When the last reference is dropped, the data is cleaned up
- Use Weak references to prevent reference cycles
Best practices:
- Use Rc when you need shared ownership
- Consider Arc for thread-safe scenarios
- Avoid creating reference cycles
Will this struct definition compile? Why or why not?
struct Philosopher { name: &str, // Reference without lifetime quote: &str, // Another reference without lifetime}
fn main() { let phil = Philosopher { name: "Seneca", quote: "Luck happens when preparation meets opportunity", };}The code fails because structs containing references must specify lifetimes. Here’s how to fix it:
// Single lifetime parameterstruct Philosopher<'a> { name: &'a str, quote: &'a str,}
// Or different lifetimes if neededstruct PhilosopherFlex<'n, 'q> { name: &'n str, quote: &'q str,}Common patterns:
// Own the data insteadstruct PhilosopherOwned { name: String, quote: String,}
// Mixed ownershipstruct PhilosopherMixed<'a> { name: String, // Owned quote: &'a str, // Borrowed}Best practices:
- Use owned types (String) when you need to store data indefinitely
- Use references when the struct’s lifetime is clearly shorter than the data
- Consider multiple lifetime parameters when references can have different lifetimes
- Document lifetime relationships in complex structures
What happens with this function that returns the longer of two string slices?
fn longest(text1: &str, text2: &str) -> &str { if text1.len() > text2.len() { text1 // Returning a reference, but which lifetime? } else { text2 // Could be this reference instead }}
fn main() { println!("{}", longest( "Seneca the Younger", "Marcus Aurelius" ));}The returned reference must remain within a lifetime valid for both inputs. It cannot outlive either borrowed value. Explicit annotations describe that relationship; they do not extend lifetimes.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b }}What happens when this code runs?
use std::cell::RefCell;
fn main() { let data = RefCell::new(42); let _borrow1 = data.borrow_mut(); // First mutable borrow let _borrow2 = data.borrow_mut(); // Second mutable borrow println!("Value: {}", _borrow2);}The question panics on the second borrow_mut() because the first mutable borrow is still alive. RefCell enforces this borrowing rule at runtime. In the corrected example below, drop(second) releases the mutable borrow before the two shared borrows.
use std::cell::RefCell;fn main() { let data = RefCell::new(42); let mut second = data.borrow_mut(); *second += 1; drop(second); let read1 = data.borrow(); let read2 = data.borrow(); println!("{} {}", *read1, *read2);}What will this code print?
use std::cell::Cell;
fn main() { let life = Cell::new(42); let meaning = &life; // Shared reference println!("{}", life.get()); // What prints here? meaning.set(43); // Mutation through shared ref println!("{}", life.get()); // And here?}Cell and RefCell serve different purposes for interior mutability:
use std::cell::{Cell, RefCell};
// Cell for Copy typesstruct Counter { count: Cell<i32>,}
impl Counter { fn increment(&self) { self.count.set(self.count.get() + 1); }}
// RefCell for non-Copy typesstruct Logger { messages: RefCell<Vec<String>>,}
impl Logger { fn log(&self, msg: &str) { self.messages.borrow_mut().push(msg.to_string()); }}Key differences:
-
Cell:
- Works best with Copy types
- No borrowing API
- Always copies or moves values
-
RefCell:
- Works with any type
- Has borrowing API
- Runtime borrow checking
Best practices:
- Use Cell for simple Copy types (numbers, bool, etc.)
- Use RefCell when you need to borrow the contents
- Keep mutations through Cell/RefCell minimal
- Document why interior mutability is needed
When should you use Rc (Reference Counting) in Rust?
Consider this example:
use std::rc::Rc;
struct SharedConfig { name: String, value: i32,}
fn main() { let config = Rc::new(SharedConfig { name: "settings".to_string(), value: 42, });
let config2 = Rc::clone(&config); // Both config and config2 share ownership}Rc (Reference Counting) is designed for single-threaded scenarios where you need shared ownership.
Common use cases:
use std::rc::Rc;use std::cell::RefCell;
// Shared ownership in data structuresstruct Node { next: Option<Rc<Node>>, value: i32,}
// Combining with interior mutabilitystruct SharedState { data: Rc<RefCell<Vec<String>>>,}
// Multiple owners of same datalet original = Rc::new(vec![1, 2, 3]);let clone1 = Rc::clone(&original);let clone2 = Rc::clone(&original);Key points:
-
Use Rc when:
- Multiple parts of your code need ownership
- You know the sharing is single-threaded
- The lifetime can’t be statically determined
-
Use Arc instead when:
- You need thread-safe sharing
- Multiple threads need ownership
-
Rc limitations:
- Not thread-safe
- Slight runtime overhead
- Can’t break reference cycles automatically
Best practices:
- Prefer unique ownership when possible
- Use Rc for single-threaded shared ownership
- Use Arc for multi-threaded scenarios
- Combine with Weak to prevent reference cycles
What happens when this code runs?
use std::sync::{Arc, Mutex};
fn main() { let lock = Arc::new(Mutex::new(42)); let lock2 = Arc::clone(&lock);
let _guard1 = lock.lock().unwrap(); // First lock let _guard2 = lock2.lock().unwrap(); // Second lock attempt
println!("Value: {}", _guard2);}Locking the same mutex twice in one thread without releasing the first guard never returns normally. The API permits a deadlock or panic; release the guard before locking again.
use std::sync::Mutex;fn main() { let lock = Mutex::new(42); { let mut guard = lock.lock().unwrap(); *guard += 1; } let guard = lock.lock().unwrap(); println!("{}", *guard);}What happens when you run this code with weak references?
use std::rc::{Rc, Weak};
fn main() { let data = Rc::new(String::from("Wisdom")); let weak = Rc::downgrade(&data); // Create weak reference drop(data); // Drop strong reference
println!("Value: {:?}", weak.upgrade());}Weak references don’t prevent deallocation of their targets. Here’s a detailed example:
use std::rc::{Rc, Weak};use std::cell::RefCell;
// Parent-child tree structure avoiding reference cyclesstruct Node { next: Option<Rc<Node>>, parent: RefCell<Weak<Node>>, // Weak to prevent cycles value: i32,}
impl Node { fn new(value: i32) -> Rc<Node> { Rc::new(Node { next: None, parent: RefCell::new(Weak::new()), value, }) }
fn set_parent(&self, parent: &Rc<Node>) { *self.parent.borrow_mut() = Rc::downgrade(parent); }
fn get_parent(&self) -> Option<Rc<Node>> { self.parent.borrow().upgrade() }}Common use cases:
- Cache-like structures where entries can be cleared
- Tree structures with parent references
- Observer patterns where subjects can be dropped
- Breaking reference cycles in complex data structures
Best practices:
- Use Weak references for optional relationships
- Check upgrade() results before using
- Document ownership relationships clearly
- Consider alternatives like indices for simpler cases
What happens to the file handle in this RAII example?
use std::fs::File;
struct FileWrapper { file: File,}
fn main() { let file = File::create("test.txt").unwrap(); let wrapper = FileWrapper { file }; // ... use wrapper ... // No Drop implementation}RAII in Rust ensures resources are properly managed. In this example, FileWrapper does not need a custom Drop implementation for the file handle to close: its File field is dropped automatically when the wrapper goes out of scope.
You only implement Drop when the wrapper itself has extra cleanup behavior beyond dropping its fields:
use std::fs::File;use std::io::{self, Write};
struct FileWrapper { file: File, path: String,}
impl FileWrapper { fn new(path: &str) -> io::Result<FileWrapper> { Ok(FileWrapper { file: File::create(path)?, path: path.to_string(), }) }
fn write(&mut self, content: &str) -> io::Result<()> { self.file.write_all(content.as_bytes()) }}
impl Drop for FileWrapper { fn drop(&mut self) { // Ensure file is properly closed // Could also do cleanup like deletion println!("Closing file: {}", self.path); }}RAII Patterns:
- Constructor acquires resources
- Methods use resources safely
- Fields are dropped automatically when the owner goes out of scope
- Custom Drop adds extra cleanup when needed
- Use
?for error propagation
Best practices:
- Rely on standard library Drop implementations when they already model the resource
- Keep resource management simple and obvious
- Use standard library types when possible
- Document cleanup behavior
- Consider using guard patterns for scoped operations
What happens when we clone this Philosophy struct?
#[derive(Clone)]struct Philosophy { school: String, founder: String,}
fn main() { let stoicism = Philosophy { school: String::from("Stoicism"), founder: String::from("Zeno of Citium") }; let new_school = stoicism.clone(); println!("{} - {}", stoicism.school, new_school.school);}Copy performs implicit bitwise copying. Clone is explicit and may allocate; here both String fields receive independent copies. Whether a value is on the stack or heap does not determine whether its type implements Copy.
On a typical current 64-bit Rust target, what’s the size of this struct?
struct Metadata { id: u32, // How many bytes? name: String, // How many bytes? active: bool // How many bytes + padding?}Let’s break down struct memory layout and optimization:
// Typical current 64-bit Rust layout: 32 bytesstruct Metadata { id: u32, // 4 bytes name: String, // 24 bytes on 64-bit systems active: bool // 1 byte + padding/alignment}
// Reordering fields may reduce padding for repr(C) structs,// but default Rust layout is not a stable ABI guarantee.struct OptimizedMetadata { name: String, // 24 bytes id: u32, // 4 bytes active: bool // 1 byte + 3 padding}
// Further optimization with packing#[repr(packed)]struct PackedMetadata { id: u32, active: bool, name: String,}Memory layout considerations:
-
Alignment requirements:
- u32: 4-byte alignment
- String: 8-byte alignment and 24-byte size on common 64-bit targets
- bool: 1-byte alignment
-
Field ordering strategies:
- Group similar-sized fields
- Put larger alignments first
- Consider cache line optimization
Best practices:
- For FFI or stable layout assumptions, use an appropriate
repr(...) - Use appropriate integer sizes
- Consider using Option for optional fields
- Measure size-critical structs with
std::mem::size_of - Use #[repr(packed)] carefully - it can affect performance
Thanks for taking the quiz! If you enjoyed testing your Rust knowledge, check out my other programming challenges! 🧠
Want to level up your Rust skills? Here are some recommended resources:
