---
title: "Rust: Ownership, Borrowing & Memory Safety"
description: "Test your knowledge of Rust fundamentals covering ownership, borrowing, lifetimes, traits, pattern matching, error handling, and memory-safe systems programming without a garbage collector."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/quizzes/post/rust-fundamentals-quiz
---

# Rust: Ownership, Borrowing & Memory Safety

This quiz tests your grip on the parts of Rust that trip up newcomers and pay off later: ownership and moves, borrowing and lifetimes, traits and generics, pattern matching, error handling with `Result`, smart pointers, fearless concurrency, and the cargo workflow. Take your time, read each option carefully, and use the hint if you get stuck.

## Questions

### 1. What happens when you assign one String to another variable in Rust, like `let b = a;`?

- Both variables become valid references to the same heap data.
  - Rust does not share ownership by default; that would be aliased mutability.
- The String is deep-cloned into the new variable automatically.
  - Cloning happens only when you call `.clone()` explicitly. Move is the default.
- **Ownership moves to the new variable and the original becomes invalid.** ✅
  - This is the move semantics that powers Rust's single-owner rule.
- The compiler refuses to compile without an explicit lifetime annotation.
  - Lifetimes apply to references, not to owned values like String.

**Explanation:** Non-Copy types like String are moved on assignment, so `a` is no longer usable after `let b = a;`.

**Hint:** Think about Rust's single-owner rule and what happens to the original binding.

### 2. Which of these types implements `Copy`, so assignment duplicates the value instead of moving it?

- String
  - String owns heap data, so it cannot be Copy.
- Vec<i32>
  - Vec owns a heap buffer and is not Copy.
- **i32** ✅
  - Primitive integers live on the stack and implement Copy.
- Box<i32>
  - Box owns its heap allocation, so it moves rather than copies.

**Explanation:** Copy is reserved for types that are trivially duplicable bit-for-bit, mostly stack-only primitives.

**Hint:** Which option has no heap allocation behind it?

### 3. What does the `Drop` trait do?

- Marks a value as garbage for a runtime collector to clean up later.
  - Rust has no garbage collector. Cleanup is deterministic at scope exit.
- **Runs custom cleanup code automatically when a value goes out of scope.** ✅
  - Implementing Drop lets you close files, release locks, free FFI handles, and so on.
- Forces the borrow checker to ignore the value.
  - Nothing legal turns off the borrow checker outside of `unsafe`.
- Returns ownership back to the caller.
  - That is what `return` or moving the value does.

**Explanation:** Drop is Rust's RAII hook. It runs deterministically when the value leaves scope.

**Hint:** Think about what happens to a `File` when it goes out of scope.

### 4. How many mutable references to the same value can exist at the same time?

- **Only one.** ✅
  - Exclusive mutable access is the rule that prevents data races at compile time.
- Up to two.
  - There is no two-reference allowance.
- Unlimited, as long as they have different lifetimes.
  - Lifetimes do not relax the aliasing rule for mutable references.
- As many as there are matching immutable references.
  - Mutable and immutable references cannot coexist on the same value.

**Explanation:** Rust enforces exclusive mutable access, which is the foundation of compile-time data-race prevention.

**Hint:** It is the rule that lets the compiler reason about aliasing.

### 5. Can a mutable reference exist at the same time as immutable references to the same value?

- Yes, mutable references override immutable ones.
  - References do not override each other; both are tracked by the borrow checker.
- **No, mutable and immutable references are mutually exclusive in the same scope.** ✅
  - This is the second half of the borrow checker's aliasing rules.
- Yes, but only inside an `unsafe` block.
  - Even `unsafe` does not legitimize undefined behavior; it just turns off compile-time checks.
- Yes, as long as the mutable reference is created first.
  - Order of creation does not bypass the rule.

**Explanation:** At any point, a value has either one mutable reference or any number of immutable ones.

**Hint:** Think about why this rule eliminates data races.

### 6. What is the borrow checker?

- A runtime check that detects null pointers.
  - Rust has no null and no runtime null check.
- **A compile-time analysis that enforces ownership and borrowing rules.** ✅
  - It rejects programs that could create dangling pointers, data races, or use-after-free at compile time.
- A linter that scans crates for unsafe code.
  - That would be `cargo geiger` or similar tools, not the borrow checker.
- A runtime garbage collector.
  - There is no garbage collector in Rust.

**Explanation:** The borrow checker runs at compile time. By the time your code runs, the rules are already proven.

**Hint:** When does it do its work?

### 7. What does `&mut T` mean?

- A raw pointer to a mutable variable.
  - Raw pointers are `*mut T` and `*const T`, used in unsafe code.
- **A mutable, exclusive reference to a value of type T.** ✅
  - While the `&mut` exists, no other reference to that value is allowed.
- A shared reference that can mutate the value.
  - Shared references (`&T`) are immutable; that is the whole point of the split.
- A boxed value of type T.
  - Boxing uses `Box<T>`, which is an owned heap allocation, not a reference.

**Explanation:** The `&mut` is the only way to borrow mutably, and it is always exclusive.

**Hint:** What does the absence of any other borrow during its lifetime guarantee?

### 8. What is the purpose of a lifetime annotation like `'a`?

- It forces the compiler to extend a value's lifetime.
  - Annotations describe relationships; they do not change actual lifetimes.
- **It tells the borrow checker how long a reference must remain valid.** ✅
  - Annotations let the borrow checker connect input and output references.
- It marks a function as a constant expression.
  - Compile-time constants use `const fn`, not lifetime annotations.
- It enables garbage collection for the value.
  - Rust has no garbage collector.

**Explanation:** Lifetimes are descriptive contracts the compiler checks; they do not influence runtime behavior.

**Hint:** What does the borrow checker need to know about references that cross function boundaries?

### 9. What does the `'static` lifetime mean?

- **The reference is valid for the entire program.** ✅
  - String literals like `"hello"` are `&'static str` because they live for the whole program.
- The value is allocated on the stack.
  - Stack allocation has nothing to do with `'static`.
- The value is immutable.
  - Immutability is a separate property; `'static mut` exists too.
- The compiler ignores its lifetime.
  - The compiler still tracks it; `'static` is just the longest possible lifetime.

**Explanation:** `'static` simply means "lives as long as the program does".

**Hint:** Think about where string literals are stored.

### 10. When can you omit lifetime annotations on function references?

- Only inside an `unsafe` block.
  - Unsafe code does not change the elision rules.
- **Whenever the compiler can infer them from the standard elision rules.** ✅
  - If each input reference gets its own lifetime and the output borrows from `self` or a single input, the compiler fills it in.
- Always. Lifetimes are entirely optional.
  - You must annotate when elision rules cannot determine the relationship.
- Only when the function has no parameters.
  - That is not how elision works.

**Explanation:** Lifetime elision is a small set of deterministic rules; if they apply, you can omit the annotations.

**Hint:** There are three lifetime elision rules; the most common cases all fit them.

### 11. What is a trait in Rust?

- A struct field.
  - Fields live on structs; traits define behavior.
- **A shared interface that types can implement, similar to an interface or type class in other languages.** ✅
  - Traits define a set of methods that implementors must provide.
- A macro that generates code.
  - Macros are separate; though `derive` macros can implement traits, traits themselves are not macros.
- A way to allocate memory on the heap.
  - Heap allocation is done with `Box`, `Vec`, and friends.

**Explanation:** Traits are how Rust models polymorphism and abstract behavior.

**Hint:** Think interface or type class.

### 12. What does `impl Trait` mean when used as a return type?

- The function returns a heap-allocated trait object.
  - That would be `Box<dyn Trait>`.
- **The function returns some concrete type that implements the trait, without naming it.** ✅
  - This is opaque return type, also known as existential return.
- The function returns the trait itself.
  - Traits are not values; you cannot return a trait directly.
- The function panics if the trait is not implemented.
  - Trait checks happen at compile time.

**Explanation:** `impl Trait` lets you hide the concrete type while guaranteeing it implements the trait.

**Hint:** It is sugar for an existential type.

### 13. What is the key difference between `dyn Trait` and `impl Trait`?

- `dyn` is faster than `impl`.
  - Static dispatch is generally faster than dynamic dispatch.
- **`dyn Trait` is dynamic dispatch through a vtable; `impl Trait` is static dispatch resolved at compile time.** ✅
  - `dyn` pays a small runtime indirection cost in exchange for object-like polymorphism.
- They are identical aliases for the same thing.
  - They generate different code with different trade-offs.
- `dyn` requires `unsafe`.
  - Both forms are perfectly safe.

**Explanation:** Choose `impl Trait` for monomorphized, zero-overhead generics; choose `dyn Trait` when you need runtime polymorphism.

**Hint:** One uses a vtable, the other monomorphizes.

### 14. What does `#[derive(Debug)]` do?

- Enables `assert!` for the type.
  - `assert!` works on booleans, not on a `Debug` impl.
- **Auto-generates an implementation of the `Debug` trait so you can format with `{:?}`.** ✅
  - Same pattern works for `Clone`, `PartialEq`, `Hash`, and other common traits.
- Marks the type for the borrow checker.
  - The borrow checker does not need a marker.
- Makes the type implement `Drop`.
  - Drop is implemented manually; it is not derivable.

**Explanation:** `derive` is a procedural macro that writes a trait impl for you when the type's fields support it.

**Hint:** What does the `{:?}` format specifier require?

### 15. What does "exhaustiveness" mean in a `match` expression?

- Every arm must include a guard clause.
  - Guards are optional refinements.
- **The match must cover every possible value of the scrutinee, often by using `_` for the rest.** ✅
  - Exhaustiveness is what makes Rust's `match` provably safe.
- Every arm must return the same value.
  - Arms must return the same TYPE, not the same value.
- The match cannot also use `if let`.
  - `if let` is a separate construct; both can be used in the same function.

**Explanation:** The compiler enforces exhaustiveness so adding a new enum variant flags every match that needs updating.

**Hint:** What guarantee does it give you when you add a new enum variant later?

### 16. When would you reach for `if let` instead of `match`?

- **When you need to handle only one specific pattern and ignore everything else.** ✅
  - `if let Some(x) = opt {}` is cleaner than a full `match` when you only care about one variant.
- When you want exhaustive case coverage.
  - That is `match`'s strength.
- When you need a return value.
  - `if let` can also be an expression that returns a value.
- It is deprecated; you should always use `match`.
  - `if let` is current, idiomatic Rust.

**Explanation:** `if let` is sugar for a `match` that only cares about one arm and ignores the rest.

**Hint:** Think readability for the single-pattern case.

### 17. What does the `_` pattern do inside `match`?

- It forces a panic.
  - That would be `unreachable!()` or `panic!()`.
- It matches anything and binds it to a variable named `_`.
  - `_` does not bind; it discards. To bind use a name.
- **It matches any value without binding it, acting as a catch-all.** ✅
  - Use `_` to satisfy exhaustiveness when you do not care about the remaining cases.
- It skips the current loop iteration.
  - That is `continue`.

**Explanation:** `_` is the wildcard pattern; it matches and discards, with no binding.

**Hint:** The key word is "wildcard".

### 18. What is the `Result<T, E>` type?

- A type that always panics on error.
  - `Result` is the alternative to panicking.
- **An enum with `Ok(T)` for success and `Err(E)` for failure.** ✅
  - Callers must handle both arms, which is what makes error handling explicit.
- A C-style errno value.
  - Rust does not use errno-style returns.
- A trait for fallible operations.
  - `Result` is a concrete enum, not a trait.

**Explanation:** Returning `Result` makes the possibility of failure visible in the function's type signature.

**Hint:** It is one of the two-variant enums you use every day.

### 19. What does the `?` operator do?

- It throws a panic immediately.
  - `?` does not panic; it propagates errors.
- **It returns early from the function on an `Err` (or `None`), propagating the value to the caller.** ✅
  - It is shorthand for `match res { Ok(v) => v, Err(e) => return Err(e.into()) }`.
- It marks the expression as unsafe.
  - Unsafe code uses the `unsafe` keyword.
- It coerces a value into `Option::Some`.
  - That is `Some(x)`.

**Explanation:** `?` is the ergonomic way to chain fallible calls without nested `match`.

**Hint:** What does it do on the `Err` branch?

### 20. When should you `panic!` instead of returning a `Result`?

- For any recoverable error.
  - Recoverable errors should return `Result` so callers can handle them.
- **For programmer bugs and unrecoverable states. `Result` is preferred for expected failures.** ✅
  - Panicking is for invariant violations like "this should never happen" or contract breaches.
- Always. `panic!` is faster than handling errors.
  - Panicking unwinds the stack and aborts; it is not a performance choice.
- Whenever the function returns an `Option`.
  - `Option` is also recoverable; handle it, do not panic.

**Explanation:** Use `Result` for fallible operations and `panic!` for genuine bugs the program cannot reasonably recover from.

**Hint:** Think about whether the error represents a bug or an expected outcome.

### 21. What does `Box<T>` do?

- Allocates a value on the stack.
  - Stack allocation is the default; Box is for the heap.
- **Allocates a value on the heap with a single owner.** ✅
  - Box is the simplest owned heap allocation in Rust.
- Provides shared, mutable access across threads.
  - That is `Arc<Mutex<T>>`, not Box.
- Skips the borrow checker.
  - Box is fully borrow-checked.

**Explanation:** Use Box when you need heap allocation, recursive types, or trait objects (`Box<dyn Trait>`).

**Hint:** Heap allocation, single owner.

### 22. What is the difference between `Rc<T>` and `Arc<T>`?

- Arc is for arrays, Rc is for single values.
  - Both work on a single value; Arc stands for Atomic Reference Counted.
- **`Rc` is single-threaded reference counting; `Arc` uses atomic counters and is safe across threads.** ✅
  - Rc is faster but is `!Send`; Arc pays for atomic operations to be thread-safe.
- They are aliases for the same type.
  - They have meaningfully different performance and safety properties.
- `Rc` is heap-allocated, `Arc` is stack-allocated.
  - Both allocate on the heap.

**Explanation:** Use Rc inside a single thread; reach for Arc only when you actually share across threads.

**Hint:** What does the "A" in Arc stand for?

### 23. What does `RefCell<T>` enable?

- Compile-time mutability checks.
  - RefCell moves those checks to runtime.
- **Interior mutability with runtime borrow checking that can panic on violation.** ✅
  - You can mutate through a `&RefCell<T>`; violating the borrow rules causes a runtime panic.
- Garbage collection.
  - Rust has no garbage collector.
- Cross-thread mutation.
  - RefCell is `!Sync`; for thread-safe interior mutability use `Mutex` or `RwLock`.

**Explanation:** RefCell is for the rare cases where you need mutation behind a shared reference in single-threaded code.

**Hint:** It moves a compile-time rule to runtime.

### 24. What do the `Send` and `Sync` marker traits mean?

- **`Send` types can be transferred across thread boundaries; `Sync` types can be safely shared between threads via references.** ✅
  - Most types are both; `Rc` is neither, `RefCell` is `!Sync`, raw pointers are typically neither.
- They are deprecated traits from early Rust.
  - Both are core to modern Rust concurrency.
- They enable network sending and synchronization protocols.
  - They are nothing to do with networking.
- They are macros that generate threading code.
  - They are marker traits, automatically derived in most cases.

**Explanation:** Send and Sync are auto-traits the compiler uses to prove your program is free of data races.

**Hint:** One is about transfer, the other is about sharing.

### 25. What's the recommended primitive in `std` for passing messages between threads?

- A `Mutex`.
  - Mutexes share state; channels pass messages.
- **A channel from `std::sync::mpsc`.** ✅
  - `mpsc::channel()` returns a `(Sender, Receiver)` pair for multi-producer single-consumer messaging.
- A raw `*mut T` pointer.
  - Raw pointers across threads would be wildly unsafe.
- `RefCell`.
  - RefCell is single-threaded.

**Explanation:** Channels follow the "do not communicate by sharing memory; share memory by communicating" pattern.

**Hint:** mpsc is in the standard library for exactly this.

### 26. Why is Rust's concurrency model called "fearless concurrency"?

- The compiler eliminates all locking automatically.
  - Locks are still your responsibility; the compiler does not insert them for you.
- **The ownership system and `Send`/`Sync` traits catch data races and many concurrency bugs at compile time.** ✅
  - If the program compiles, it is statically free of data races on safe Rust types.
- Rust does not actually support concurrency.
  - Rust has full threading, channels, async, and more.
- Threads cannot crash a Rust program.
  - A panic in a thread can still abort or be joined and observed.

**Explanation:** The phrase captures Rust's promise: you can refactor concurrent code without fearing data races sneaking in.

**Hint:** What does the compiler prove for you?

### 27. What is `cargo`?

- A test runner only.
  - Testing is one feature; cargo does much more.
- **Rust's package manager and build tool. It also runs tests, builds docs, and publishes crates.** ✅
  - `cargo new`, `cargo build`, `cargo test`, `cargo run`, `cargo publish` are the bread-and-butter commands.
- A linter for Rust code.
  - That is `clippy`, run via `cargo clippy`.
- A virtual machine for Rust programs.
  - Rust compiles to native code; there is no VM.

**Explanation:** Cargo is the central entry point for working with Rust projects.

**Hint:** Think build + dependencies + everything in between.

### 28. What is `Cargo.toml`?

- **A configuration file declaring a package's metadata, dependencies, and build settings.** ✅
  - It is the manifest that cargo reads to know how to build and resolve dependencies.
- A Rust source file.
  - Source files have `.rs` extensions.
- A binary artifact produced by the compiler.
  - Build artifacts live in `target/`.
- A lockfile that pins exact dependency versions.
  - That is `Cargo.lock`, a separate file.

**Explanation:** `Cargo.toml` is the manifest; `Cargo.lock` is the lockfile.

**Hint:** It is the package's declaration; another file is the lockfile.
