Answer from a language model, not from memorized syntax: name the mechanism, show a small example, and explain the production consequence.
Question set
12 detailed answers
01How does Rust guarantee memory safety without a garbage collector?
concept
Short answer: Rust checks ownership and borrows statically, at compile time. The borrow checker proves every reference lives no longer than its data and that there's never simultaneous aliasing plus mutation — so use-after-free, double-free and data races are ruled out before the program ever runs. No GC and no reference counting by default: you pay in compile time and learning curve.
In depth:
- Ownership — every value has exactly one owner; when it leaves scope the memory is freed deterministically (
Drop), with no runtime pauses. - Borrowing — the compiler enforces "either many
&Tor one&mut T", which is what kills data races statically. - Lifetimes — the borrow checker matches the lifetimes of references and data, preventing a dangling reference from ever existing.
- Zero-cost abstractions — the compiler pays for safety; there are no checks in the binary.
| Criterion | Rust | GC languages |
|---|---|---|
| When checked | compile time | runtime |
| Runtime cost | zero | GC pauses / refcount |
| Failure mode | compile error | leaks, latency, rare bugs |
⚠️ Common mistake: answering "Rust counts references, like ARC in Swift" or "there's a GC under the hood". By default there's neither — Rc/Arc are an opt-in tool, not the memory-safety mechanism.
02Name Rust's three ownership rules. What happens to a value when its owner goes out of scope?
junior
Short answer: (1) Every value has an owner; (2) there is exactly one owner at a time; (3) when the owner goes out of scope, the value is dropped. On leaving scope Rust automatically runs Drop and frees the resources — deterministically and with no garbage collector.
In depth:
- One owner — assignment or passing into a function moves ownership; the old binding becomes unusable.
- Exactly one at a time — shared access goes through borrowing (
&/&mut), not through a second owner. - End of scope → drop — at the closing
}the destructor runs in reverse order of construction; this is a purely static decision by the compiler, not runtime tracking.
fn main() {
let a = String::from("hi"); // a owns the heap buffer
{
let b = String::from("bye");
// ...
} // <- b is dropped here
let c = a; // move: a is no longer valid
} // <- c is dropped here (former a's buffer); a was already moved
⚠️ Common mistake: describing drop as "GC-like runtime tracking". The compiler knows the destructor's timing statically — from scope and move rules, not from a reference count.
03What physically happens when a value is moved — is it a deep copy?
junior
Short answer: No. A move is a shallow bitwise copy of the value's stack representation (for a Vec that's pointer + length + capacity, 24 bytes on x86-64) plus the compiler invalidating the old binding. The heap data doesn't move or get copied — only the right to own it changes hands.
In depth:
- Only the stack part is copied — three machine words (
ptr,len,cap), not the buffer itself. - The heap stays put — the same allocated buffer now belongs to the new binding.
- The old binding is invalidated — using it after the move is a compile error, so the buffer keeps exactly one owner (no double-free).
let v = vec![1,2,3]; let w = v; (move)
stack: v [ptr|len=3|cap=3] stack: v ✗ (unusable)
│ w [ptr|len=3|cap=3]
▼ │
heap: [1][2][3] ◄─────────── same buffer ─┘
⚠️ Common mistake: thinking a move "copies the data" or is an expensive operation. It copies a fixed 24 bytes off the stack regardless of the Vec's size — the heap buffer is never touched.
04What's the difference between Copy and Clone? Why can't Vec<T> be Copy?
junior
Short answer: Copy is a marker trait: the value is duplicated by implicit bitwise copy, no code runs, and assignment semantics switch from move to copy. Clone is explicit duplication via .clone() that can be arbitrarily expensive. Vec<T> can't be Copy because it owns a heap buffer: a bitwise copy would give two owners of one allocation → double-free.
In depth:
- Copy = bitwise copy — only for types that live entirely on the stack (
i32,bool,char,&T); assignment no longer invalidates the source. - Clone = explicit logic — the
clonemethod can allocate and deep-copy (forVec, allocate a fresh buffer and copy the elements). - Why not
Vec— a bitwise copy would copy the pointer, not the data; twoVecs would point at one buffer and both would runDrop.
Copy |
Clone |
|
|---|---|---|
| Invocation | implicit, on = |
explicit, .clone() |
| Cost | bitwise, cheap | arbitrary |
| What it does | move → copy | duplicates, heap too |
For Vec<T> |
✗ forbidden | ✓ (deep copy) |
⚠️ Common mistake: thinking Copy "duplicates data". It adds no copying — it just switches assignment semantics from move to bitwise copy for types where that's safe.
05Why can't a type implement both Copy and Drop?
middle
Short answer: They're mutually exclusive contracts, and the compiler forbids combining them with error E0184. Copy means "the value may be silently duplicated bitwise", while Drop means "the type has custom resource cleanup". Two bitwise copies would both run Drop over the same resource → double-free.
In depth:
Copyimplies no owned resource — the type is trivial enough that a duplicate is indistinguishable from the original.Dropimplies unique ownership — the destructor must run exactly once for exactly one owner.- The conflict — if the compiler allowed both,
let b = a;would make a copy, and at scope exitDropwould run for bothaandb.
#[derive(Clone)]
struct Handle(i32);
impl Copy for Handle {} // error[E0184]: the trait `Copy`
impl Drop for Handle { // cannot be implemented for this type;
fn drop(&mut self) {} // the type has a destructor
}
⚠️ Common mistake: trying to slap #[derive(Copy)] onto a type with a manual Drop to "get rid of moves". You have to pick one: either the type is a plain value (Copy), or a resource owner with cleanup (Drop).
06Why won't the compiler let you call value.drop() directly, and how do you free a resource early?
middle
Short answer: Drop::drop takes &mut self, not self — the value stays alive after the call, and at scope end the destructor would run a second time (double-free). So the compiler forbids calling .drop() explicitly. To free a resource early you use std::mem::drop(value) — a plain function that takes the value by ownership and lets it drop.
In depth:
- The signature is the obstacle — if
.drop()were allowed, the binding would stay valid and Rust would runDropagain at}. - The idiom is
mem::drop— it's literallypub fn drop<T>(_x: T) {}: it takesTby value (move), the body is empty, and at its}the object is destroyed exactly once. - The effect — ownership moves into the function, the original binding is invalidated, and the resource is freed immediately.
let guard = lock.lock().unwrap();
// guard.drop(); // ✗ error[E0040]: explicit use of destructor method
drop(guard); // ✓ releases the mutex right here
// ... further code without holding the lock
⚠️ Common mistake: calling value.drop() as a method. The right way is the free function drop(value) that takes ownership; the manual Drop::drop is not meant to be called directly at all.
07State the borrowing rule: why any number of &T but only one &mut T at a time?
junior
Short answer: The rule is "aliasing XOR mutability": at any moment either any number of shared &T references, or exactly one exclusive &mut T, but never both at once. This statically rules out data races and iterator invalidation, and it also lets LLVM treat &mut T as noalias and optimize aggressively.
In depth:
- Many
&T— readers don't interfere with each other, so an unlimited number of shared references is safe. - One
&mut T— a writer must be certain no one else is reading or writing the same place concurrently; hence exclusivity. - Why so strict — combine reading and writing and you get a data race, or a reference into a
Vecbuffer that was already reallocated. - Optimization — the
noaliasguarantee frees the compiler up, which is why breaking the rule viaunsafeis UB, not a style question.
| Simultaneously | Allowed? |
|---|---|
many &T |
✓ |
one &mut T |
✓ |
&T and &mut T together |
✗ |
two &mut T |
✗ |
⚠️ Common mistake: thinking the rule is "you can't mutate data while it's read, purely for convenience". It's about correctness: simultaneous aliasing plus mutation is UB, and the compiler relies on its absence.
08Why does the compiler forbid returning a reference to a local variable?
junior
Short answer: A local variable is destroyed when the function returns — its stack frame unwinds and the memory is freed. A reference to it would dangle, pointing at garbage. The borrow checker proves that data must outlive the references to it, can't prove that here, and rejects the code (E0515: cannot return reference to local variable).
In depth:
- Drop timing — at the function's closing
}the local is dropped; returning a reference to it means handing out a pointer to already-freed memory. - What the compiler checks — the referent's lifetime must cover the reference's lifetime; for a local it ends before the reference could ever be used. Note that a signature with no input references and no explicit lifetime (
fn bad() -> &String) fails even earlier, at elision — E0106 "missing lifetime specifier". - How to fix — return the value by ownership (move it out), not a reference; the caller becomes the owner.
fn bad<'a>() -> &'a String {
let s = String::from("hi");
&s // ✗ error[E0515]: cannot return reference to local variable `s`
} // s is dropped here → the reference would dangle
fn good() -> String {
let s = String::from("hi");
s // ✓ hand ownership out
}
⚠️ Common mistake: answering just "it won't compile". The interviewer wants the drop-timing reasoning: the local dies on frame exit, so the reference would be guaranteed to dangle.
09What are lifetimes and how do the three elision rules work?
middle
Short answer: A lifetime is the region of code over which a reference must stay valid; it's a label for the borrow checker, not a runtime duration. Elision is a set of three rules by which the compiler fills in lifetimes for typical signatures so you don't have to write them by hand.
In depth:
- Rule 1 — each elided input reference gets its own distinct lifetime (
fn f(a: &X, b: &Y)→ two different ones). - Rule 2 — if there is exactly one input lifetime, it's assigned to all output references.
- Rule 3 — if
&self/&mut selfis among the arguments, its lifetime goes to all output references (the common case for methods).
If after applying the rules some output reference still has an undetermined lifetime, the compiler demands an explicit annotation.
| Elided form | What the compiler sees |
|---|---|
fn f(s: &str) -> &str |
fn f<'a>(s: &'a str) -> &'a str |
fn g(x: &i32, y: &i32) |
fn g<'a,'b>(x: &'a i32, y: &'b i32) |
fn m(&self, o: &str) -> &str |
output takes &self's lifetime |
⚠️ Common mistake: thinking lifetime annotations affect how long data lives. They neither extend nor shorten anything — they only describe existing lifetime relationships to the borrow checker.
10What does 'static mean as a reference lifetime vs as a T: 'static bound — e.g. in thread::spawn?
middle
Short answer: &'static T means the data lives for the whole program (string literals, say, are baked into the binary). T: 'static means the type contains no references shorter than 'static — i.e. "can live for any length of time", not "lives forever". thread::spawn requires F: Send + 'static because the thread may outlive the frame that spawned it.
In depth:
'staticas a reference lifetime — a concrete guarantee: the referent is valid until the program ends. Example:let s: &'static str = "hi";.T: 'staticas a bound — a constraint on the type: it holds no borrows that could go stale. Owning types (String,Vec<i32>) satisfy it automatically.- Why
spawnneeds it — the closure is handed to a new thread that may keep running after the spawning function returns; so it must not hold references to that function's locals.
| Form | Meaning |
|---|---|
&'static T |
"this data lives until the program ends" |
T: 'static |
"T contains no references shorter than 'static" → may live any length |
⚠️ Common mistake: conflating the two meanings and thinking T: 'static forces the value to live forever. It only forbids short-lived borrows inside T; a String itself is happily dropped when it leaves scope.
11What are NLL (non-lexical lifetimes) and how did they change the borrow checker?
middle
Short answer: NLL is a model where a borrow ends at the reference's last use, not at the closing } of the lexical block. It was stabilized in Rust 2018, and thanks to it code that "violates" the textbook rule by the letter now compiles fine.
In depth:
- Before NLL — a reference's lifetime stretched to the end of the lexical scope, even if nothing touched it after its last use.
- With NLL — the borrow checker computes the borrow region from the actual control flow and cuts it off right after the last access.
- The effect — you can take
&x, use it, then take&mut xin the same block; patterns like conditional borrows and early returns stopped being falsely rejected.
let mut v = vec![1, 2, 3];
let first = &v[0]; // ─┐ borrow begins
println!("{first}"); // ─┘ last use → it ends right here (NLL)
v.push(4); // ✓ OK: &v[0] is no longer active
// pre-NLL: ✗ E0502 — first "lived" to the end of the block
⚠️ Common mistake: insisting a snippet won't compile based on the old "borrow lasts to the end of the block" rule. With NLL the borrow ends at its last use — and such code is valid.
12What is variance in Rust: why is &'a T covariant but &mut T invariant in T?
senior
Short answer: Variance describes how subtyping of one type carries over to a compound type. &'a T is covariant in T: a reference to a "longer-lived/more specific" type can be substituted where a weaker reference is expected — reading from it is safe. &mut T is invariant in T: you can also write through it, and a write would let a short-lived value be smuggled into a long-lived slot, so subtyping is forbidden in both directions.
In depth:
- Covariance (
&T) — read-only, nothing is placed inside; substituting&'longwhere&'shortis needed is safe ('long: 'short). - Invariance (
&mut T) — bidirectional access. Allow covariance here and you could write a short reference through&mut &'shortinto a cell of type&'long, leaving a dangling reference exposed afterwards. Cell/RefCell— invariant for the same reason: interior mutability grants writes, so variance can't be narrowed.
| Type | Variance in T |
Why |
|---|---|---|
&'a T |
covariant | read-only — stronger→weaker substitution is safe |
&'a mut T |
invariant | writable — otherwise a short lifetime could be smuggled in |
Cell<T>/RefCell<T> |
invariant | interior mutability = writes |
⚠️ Common mistake: assuming &mut T is covariant "because a reference to a subtype is still a reference". Covariance breaks precisely because of writes: it would let short-lived data be stuffed into a long-lived cell and produce UB.
Source notes
References and review policy
RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.