Rust Ownership Practice (Interactive)
Intuition
Learning through practice: Practice problems are like training drills, they help you apply knowledge and identify areas that need more study.
Why it matters: Regular practice builds confidence and reveals patterns in how concepts are tested. Each problem reinforces key programming concepts.
The key insight: Making mistakes during practice is valuable, each error points to a concept that needs clarification.
Rust, Ownership Practice
8 auto-graded practice problems covering Rust ownership, lifetimes, references, and smart pointers. Select an answer, submit, and review the explanation.
Ownership
Q1. What happens when you write let x = String::from("hello"); let y = x; println!("{}", x);?
Answer: A
Q1. Which of the following will compile without errors?
A. let x = 5; let y = x; println!(”{}
B. hi”); let y = x; println!(”{}”, x); C. let x = vec![1]; let y = x; x.push(2); D. let r1 = &mut s; let r2 = &mut s;’,
Answer: A
- Integer types like
i32implement theCopytrait, solet y = xcopies the value rather than moving it.xremains valid and can still be used.StringandVecdo not implementCopy, so the other options fail. Difficulty: medium
Lifetimes
Q2. What does the lifetime annotation 'a in fn longest<'a>(x: &'a str, y: &'a str) -> &'a str signify?
A. The returned reference lives at least as long as the shorter of the two input lifetimes B. The returned reference lives exactly as long as both inputs C. The function returns a static reference D. Memory is leaked after the function returns’,
Answer: A
- The lifetime
'atells the compiler that the returned reference will be valid for at least as long as the shorter of the two input lifetimes. This means the caller can use the result as long as both input references are still valid. Difficulty: medium
Q3. Why does fn first_word(s: &str) -> &str compile without explicit lifetime annotations?
A. Lifetime elision rules automatically assign the same lifetime to all references’, “Rust doesn’t check lifetimes for simple functions B. It won’t compile without annotations”,
Answer: A
- Rust has lifetime elision rules. When there is exactly one input lifetime, it is assigned to all output references. So
fn first_word(s: &str) -> &stris equivalent tofn first_word<'a>(s: &'a str) -> &'a str. Difficulty: easy
References
Q4. Can you have both a mutable reference and an immutable reference to the same data at the same time?
A. No, mutable references are exclusive B. Yes, as long as the immutable reference was created first C. Yes, but only in unsafe blocks D. Yes, Rust automatically handles conflicts’,
Answer: A
- Rust’s borrowing rules state that you can have any number of immutable references OR exactly one mutable reference, but not both simultaneously. This prevents data races at compile time. Difficulty: easy
Q5. What is reborrowing in Rust?
A. Creating a new reference from an existing reference without transferring the original borrow B. Moving a reference to a new variable C. Dereferencing a pointer to get the underlying value D. Casting a mutable reference to an immutable one’,
Answer: A
- Reborrowing occurs when you create a new reference from an existing reference (e.g.,
&*xor passing&xwherexis already&mut T). The original borrow remains valid and will be valid again after the reborrow ends. Difficulty: hard
Smart Pointers
Q6. What is the key difference between Box<T> and Rc<T>?
A. Box has single ownership; Rc enables shared ownership via reference counting B. Box is for stack allocation; Rc is for heap allocation C. Box is thread-safe; Rc is not D. There is no difference’,
Answer: A
Box<T>provides single ownership of heap data, only one owner at a time.Rc<T>(Reference Counted) allows multiple owners of the same data by keeping a count of active references. Neither is thread-safe; useArc<T>for shared ownership across threads. Difficulty: medium
Q7. When should you use Arc<T> instead of Rc<T>?
A. When you need to share data across multiple threads B. When you need mutable access to shared data C. When you want to avoid heap allocation D. When the data is very small’,
Answer: A
Arc<T>(Atomic Reference Counted) is the thread-safe version ofRc<T>. It uses atomic operations for reference counting, making it safe to share across threads. UseArcwhen data needs to be accessed from multiple threads. For interior mutability with Arc, combine withMutex<T>orRwLock<T>. Difficulty: medium
Borrowing
Q8. Why does the following code fail to compile? let mut v = vec![1,2,3]; let first = &v[0]; v.push(4);
A. push may reallocate, invalidating the immutable reference `first B. Cannot push to a vector after borrowing C. first is a copy, not a reference D. push requires exclusive access but v is not mutable’,
Answer: A
- push may cause reallocation of the vector’s backing buffer if capacity is exceeded. This would invalidate the pointer
firstpoints to. Rust prevents this by refusing to allow a mutable borrow (via push) while an immutable reference (first) is still live. Difficulty: hard
Lifetimes
Q9. What lifetime annotation is needed for fn first_or_default(s: &[str]) -> &str?
A. None — lifetime elision handles it B. fn first_or_default<‘a>(s: &‘a str) -> &‘a str C. fn first_or_default<‘a>(s: &[str]) -> &‘a str D. fn first_or_default(s: &str) -> &str is invalid’,
Answer: A
- With one input lifetime, elision rules assign that lifetime to all output references. So the explicit form is fn first_or_default<‘a>(s: &‘a str) -> &‘a str, but the compiler handles it automatically. Difficulty: easy
Trait Objects
Q10. What is the size of a Box<dyn Display> compared to a Box<i32>?
A. Box<dyn Display> is larger (fat pointer with vtable); Box<i32> is a single pointer B. They are the same size C. Box<dyn Display> is smaller D. dyn Display is unsized, so Box cannot hold it’,
Answer: A
- Box<dyn Display> is a fat pointer: it holds both a data pointer and a vtable pointer (2 usize). Box<i32> is a thin pointer to a single i32 on the heap (1 usize). The fat pointer enables dynamic dispatch at runtime. Difficulty: medium
Smart Pointers
Q11. What does RefCell<T> provide that Box<T> does not?
A. Interior mutability with runtime borrow checking B. Compile-time borrow checking C. Thread-safe shared ownership D. Automatic garbage collection’,
Answer: A
- RefCell<T> provides interior mutability: you can modify the inner value even through an immutable reference. It enforces borrow rules at runtime (panics if violated) rather than compile time. Box<T> enforces exclusive ownership with no interior mutability. Difficulty: medium
Move Semantics
Q12. What happens when you move a String into a function and then try to use it after the call?
A. Compilation error: value moved and no longer usable B. The string is copied automatically C. The string is still valid but empty D. Runtime error: use after free’,
Answer: A
- String does not implement Copy, so moving it into a function transfers ownership. After the move, the original variable is invalid. To use it after the call, you must clone it first, take a reference, or return the value from the function. Difficulty: easy
Trait Objects and Generics
Q13. What is the difference between impl Trait and dyn Trait in a function return type?
A. impl Trait returns a concrete type known at compile time; dyn Trait returns a trait object with dynamic dispatch B. They are identical C. dyn Trait is faster D. impl Trait allows multiple return types’,
Answer: A
- impl Trait hides a specific type known at compile time (static dispatch, monomorphized). dyn Trait uses dynamic dispatch via a fat pointer (vtable). impl Trait cannot return different types from different branches; dyn Trait can return any type implementing the trait. Difficulty: medium
Q14. Why does fn first_word(s: &str) -> &str compile without lifetime annotations?
A. Lifetime elision rules infer the lifetime automatically B. Rust does not check lifetimes for &str C. It returns a static lifetime D. The compiler guesses the lifetime’,
Answer: A
- Lifetime elision rules state that when there is exactly one input lifetime, it is assigned to all output references. So the explicit form fn first_word<‘a>(s: &‘a str) -> &‘a str is automatically inferred. Difficulty: easy
Enums and Pattern Matching
Q15. What does if let Some(x) = option_value { ... } do?
A. Executes the block only if the Option is Some, binding x to the inner value B. Always executes C. Unwraps the Option or panics D. Converts Option to Result’,
Answer: A
- if let is a pattern match that handles only one variant. If option_value is Some(x), x is bound to the inner value and the block executes. If it is Nothing, the block is skipped. It is a concise alternative to a full match statement. Difficulty: easy
Common Mistakes
Assuming Copy types are moved: Primitive types like i32 and bool implement Copy and are copied, not moved. But structs and String are moved by default. Forgetting which types copy vs move causes confusion.
Confusing Box, Rc, and Arc: Box is single-owner heap allocation; Rc is single-threaded shared ownership; Arc is multi-threaded shared ownership. Using the wrong one causes compile errors or data races.
Forgetting that push may reallocate: Calling push on a Vec while holding an immutable reference to its contents invalidates the reference. Rust prevents this at compile time to avoid dangling pointers.
Cross-References
- Rust Basics: Core Rust language features including syntax, control flow, and functions.
- Rust Traits: Trait definitions, implementations, and generic constraints.
- Rust Concurrency: Threads, channels, and async/await patterns.
- C++ Pointers: Manual memory management for comparison with Rust’s ownership model.
- Go Basics: Simpler memory model with garbage collection for comparison.
- Rust Book: Official Rust documentation and tutorials.
Advanced Content
This section provides detailed coverage of advanced concepts, including full derivations, proofs, and extended examples.
Derivations and Proofs
Complete mathematical derivations and proofs are provided where appropriate. Each step is explained to ensure understanding of the underlying reasoning.
Extended Examples
Advanced examples demonstrate the application of concepts to complex problems. These examples go beyond standard exam questions to develop deeper understanding.
Research Connections
This material connects to current research and advanced applications in the field. Understanding these connections provides context for the study material.
Prerequisites
Ensure you have mastered the prerequisite material before attempting this advanced content.