Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Ownership & Borrowing
RUST

Ownership & Borrowing

Practice 13 Rust questions covering ownership, borrowing, references, moves, copies, borrowing rules, and memory safety.

1. What is the fundamental rule of variable mutability in Rust by default?

Answer: Variables are immutable by default and must be explicitly declared with mut.

In Rust, let x = 5; creates a variable you cannot reassign:

let x = 5;
x = 6;   // compile error: cannot assign to immutable variable
let mut y = 5;
y = 6;   // OK — y is mutable

This immutability-by-default is a safety design: it removes an entire class of accidental-mutation bugs at compile time, and it lets the compiler reason safely about aliasing and concurrency. If you need to change a binding, you opt in with mut. (Note: mut controls the binding — the variable’s ability to be reassigned — separate from the mutability of the data a reference points to.)

The interview answer: variables are immutable by default; mut explicitly opts into mutability.

Answer:

Variables are immutable by default and must be explicitly declared with mut.

In Rust, let x = 5; creates a variable you cannot reassign:

let x = 5;
x = 6;   // compile error: cannot assign to immutable variable
let mut y = 5;
y = 6;   // OK — y is mutable

This immutability-by-default is a safety design: it removes an entire class of accidental-mutation bugs at compile time, and it lets the compiler reason safely about aliasing and concurrency. If you need to change a binding, you opt in with mut. (Note: mut controls the binding — the variable’s ability to be reassigned — separate from the mutability of the data a reference points to.)

The interview answer: variables are immutable by default; mut explicitly opts into mutability.

2. Which rule best describes Rust’s borrowing rules for references to a resource at any given time?

Answer: Either one mutable reference OR any number of immutable references — never both.

Rust’s aliasing rule, enforced entirely at compile time:

  • Any number of immutable references (&T) may coexist.
  • Exactly one mutable reference (&mut T) — and while it exists, no other reference (mutable or immutable) is allowed.

This is “aliasing XOR mutability”: you can’t have multiple writers, and you can’t read through one alias while another could be writing. The borrow checker validates this statically for the entire lifetime of each reference, so data races become a compile error instead of a runtime hazard.

let mut v = String::from("hi");
let r1 = &v;          // OK
let r2 = &v;          // OK — multiple immutable refs fine
let r3 = &mut v;      // ERROR — r1, r2 still alive

The interview answer: one &mut T OR unlimited &T at any time — never both — enforced at compile time to prevent data races.

Answer:

Either one mutable reference OR any number of immutable references — never both.

Rust’s aliasing rule, enforced entirely at compile time:

  • Any number of immutable references (&T) may coexist.
  • Exactly one mutable reference (&mut T) — and while it exists, no other reference (mutable or immutable) is allowed.

This is “aliasing XOR mutability”: you can’t have multiple writers, and you can’t read through one alias while another could be writing. The borrow checker validates this statically for the entire lifetime of each reference, so data races become a compile error instead of a runtime hazard.

let mut v = String::from("hi");
let r1 = &v;          // OK
let r2 = &v;          // OK — multiple immutable refs fine
let r3 = &mut v;      // ERROR — r1, r2 still alive

The interview answer: one &mut T OR unlimited &T at any time — never both — enforced at compile time to prevent data races.

3. What happens to a value when its owning variable goes out of scope in Rust?

Answer: Rust automatically calls the value’s drop to free its resources immediately — no garbage collector.

Rust uses RAII (Resource Acquisition Is Initialization), the same pattern C++ uses: ownership of a resource is tied to the lifetime of a variable. When the owner goes out of scope, the compiler inserts a call to Drop::drop at that point — freeing heap memory, closing files, releasing locks, etc., deterministically and immediately.

{
    let s = String::from("hello");   // allocates on the heap
}                                    // s goes out of scope → drop runs → memory freed

Key consequences:

  • No runtime garbage collector — cleanup happens at a known point in the code.
  • Resources are freed when the owner dies, and the compiler guarantees drop runs exactly once.
  • You can implement the Drop trait to run custom cleanup, but the memory management is automatic.

The interview answer: when an owner goes out of scope, drop is invoked automatically, freeing resources immediately (RAII, no GC).

Answer:

Rust automatically calls the value’s drop to free its resources immediately — no garbage collector.

Rust uses RAII (Resource Acquisition Is Initialization), the same pattern C++ uses: ownership of a resource is tied to the lifetime of a variable. When the owner goes out of scope, the compiler inserts a call to Drop::drop at that point — freeing heap memory, closing files, releasing locks, etc., deterministically and immediately.

{
    let s = String::from("hello");   // allocates on the heap
}                                    // s goes out of scope → drop runs → memory freed

Key consequences:

  • No runtime garbage collector — cleanup happens at a known point in the code.
  • Resources are freed when the owner dies, and the compiler guarantees drop runs exactly once.
  • You can implement the Drop trait to run custom cleanup, but the memory management is automatic.

The interview answer: when an owner goes out of scope, drop is invoked automatically, freeing resources immediately (RAII, no GC).

4. What will happen when trying to compile the following code?

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}", s1);
}

Answer: A compile-time error — “use of moved value: s1”.

String does not implement the Copy trait (it owns heap memory), so let s2 = s1; is a move: ownership of the string transfers from s1 to s2. After the move, s1 is invalid — the compiler tracks this and rejects println!("{}", s1) with a compile error about using a moved value.

This is Rust’s ownership system preventing a double-free: without moves, both s1 and s2 would try to free the same heap buffer at scope exit. The fix options:

  • Don’t use s1 afterward.
  • Clone: let s2 = s1.clone(); — copies the data (deep copy).
  • Borrow: let s2 = &s1; — reference instead of ownership transfer.

Note: Copy types (integers, bool, char) don’t move — let a = b; copies them, and both remain usable. String isn’t Copy, so it moves. The interview answer: compile error — s1 was moved into s2, so using s1 afterward is rejected.

Answer:

A compile-time error — “use of moved value: s1”.

String does not implement the Copy trait (it owns heap memory), so let s2 = s1; is a move: ownership of the string transfers from s1 to s2. After the move, s1 is invalid — the compiler tracks this and rejects println!("{}", s1) with a compile error about using a moved value.

This is Rust’s ownership system preventing a double-free: without moves, both s1 and s2 would try to free the same heap buffer at scope exit. The fix options:

  • Don’t use s1 afterward.
  • Clone: let s2 = s1.clone(); — copies the data (deep copy).
  • Borrow: let s2 = &s1; — reference instead of ownership transfer.

Note: Copy types (integers, bool, char) don’t move — let a = b; copies them, and both remain usable. String isn’t Copy, so it moves. The interview answer: compile error — s1 was moved into s2, so using s1 afterward is rejected.

5. What will be the output of this code snippet?

fn main() {
    let mut x = 5;
    let y = &x;
    let z = &x;
    println!("{} and {}", y, z);
}

Answer: Compiles and prints 5 and 5.

The borrow rule allows any number of immutable references to coexist. Here y and z are both &x (immutable borrows) — two immutable references to the same value, which is perfectly legal. The mut on x only matters if you later need &mut x; it doesn’t prevent shared immutable borrows.

So the code compiles cleanly and prints 5 and 5. (If you then added let w = &mut x; while y/z were still alive, that would be a compile error — a mutable borrow while immutable borrows exist.) The interview answer: compiles fine, prints 5 and 5 — multiple immutable borrows are allowed.

Answer:

Compiles and prints 5 and 5.

The borrow rule allows any number of immutable references to coexist. Here y and z are both &x (immutable borrows) — two immutable references to the same value, which is perfectly legal. The mut on x only matters if you later need &mut x; it doesn’t prevent shared immutable borrows.

So the code compiles cleanly and prints 5 and 5. (If you then added let w = &mut x; while y/z were still alive, that would be a compile error — a mutable borrow while immutable borrows exist.) The interview answer: compiles fine, prints 5 and 5 — multiple immutable borrows are allowed.

6. What are Rust’s explicit lifetime annotations (e.g., ‘a) used for by the compiler?

Answer: They let the borrow checker verify references don’t outlive their data, describing relationships between references — with zero runtime cost.

Lifetimes are purely compile-time. 'a annotates a reference’s validity scope and, more importantly, relates multiple references:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

The signature says: x and y both live at least as long as 'a, and the returned reference is valid as long as 'a too. The borrow checker uses this to guarantee the returned reference points at memory that’s still alive wherever the caller uses it — making dangling references impossible in safe Rust.

Key points:

  • Zero runtime cost — annotations vanish in the compiled code; they’re only for the compiler.
  • The compiler also elides lifetimes in common cases (you rarely write them).
  • They express relationships, not concrete durations.

The interview answer: lifetime annotations let the borrow checker prove references stay valid as long as they’re used; purely compile-time, no runtime overhead.

Answer:

They let the borrow checker verify references don’t outlive their data, describing relationships between references — with zero runtime cost.

Lifetimes are purely compile-time. 'a annotates a reference’s validity scope and, more importantly, relates multiple references:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

The signature says: x and y both live at least as long as 'a, and the returned reference is valid as long as 'a too. The borrow checker uses this to guarantee the returned reference points at memory that’s still alive wherever the caller uses it — making dangling references impossible in safe Rust.

Key points:

  • Zero runtime cost — annotations vanish in the compiled code; they’re only for the compiler.
  • The compiler also elides lifetimes in common cases (you rarely write them).
  • They express relationships, not concrete durations.

The interview answer: lifetime annotations let the borrow checker prove references stay valid as long as they’re used; purely compile-time, no runtime overhead.

7. What is a “dangling reference” in Rust, and how does Rust prevent it?

Answer: A reference pointing at deallocated/out-of-scope memory; Rust’s borrow checker rejects such code at compile time.

A dangling reference is the classic C/C++ bug: a reference (or pointer) that outlives the data it points to — the data is freed or its scope ends, but the reference is still used. In Rust:

fn dangle() -> &String {        // compile error
    let s = String::from("hi");
    &s                          // s dies when the function returns
}

The borrow checker rejects this at compile time — it proves a reference can’t outlive its referent using lifetimes, so the error appears before the program ever runs. No dangling references are possible in safe Rust; unsafe code must uphold the rule manually. The interview answer: a reference to freed/out-of-scope memory; the borrow checker plus lifetime analysis prevents it at compile time.

Answer:

A reference pointing at deallocated/out-of-scope memory; Rust’s borrow checker rejects such code at compile time.

A dangling reference is the classic C/C++ bug: a reference (or pointer) that outlives the data it points to — the data is freed or its scope ends, but the reference is still used. In Rust:

fn dangle() -> &String {        // compile error
    let s = String::from("hi");
    &s                          // s dies when the function returns
}

The borrow checker rejects this at compile time — it proves a reference can’t outlive its referent using lifetimes, so the error appears before the program ever runs. No dangling references are possible in safe Rust; unsafe code must uphold the rule manually. The interview answer: a reference to freed/out-of-scope memory; the borrow checker plus lifetime analysis prevents it at compile time.

8. Which keyword is used to enter an unchecked environment where Rust’s safety guarantees are relaxed?

Answer: unsafe.

unsafe is Rust’s escape hatch — it opens a block, function, or trait impl where you take on responsibility the compiler normally enforces. It grants five superpowers:

  1. Dereference a raw pointer (*const T, *mut T).
  2. Call an unsafe function or method.
  3. Access/modify a mutable static.
  4. Implement an unsafe trait.
  5. Access fields of a union.

Crucially, unsafe does not disable all checks or make code “untyped” — it only relaxes specific guarantees, and the programmer must uphold the safety invariants manually (which is why it should be wrapped in safe APIs and heavily commented). The interview answer: unsafe — a scoped opt-out from some of Rust’s safety guarantees.

Answer:

unsafe.

unsafe is Rust’s escape hatch — it opens a block, function, or trait impl where you take on responsibility the compiler normally enforces. It grants five superpowers:

  1. Dereference a raw pointer (*const T, *mut T).
  2. Call an unsafe function or method.
  3. Access/modify a mutable static.
  4. Implement an unsafe trait.
  5. Access fields of a union.

Crucially, unsafe does not disable all checks or make code “untyped” — it only relaxes specific guarantees, and the programmer must uphold the safety invariants manually (which is why it should be wrapped in safe APIs and heavily commented). The interview answer: unsafe — a scoped opt-out from some of Rust’s safety guarantees.

9. Which of the following operations is allowed exclusively inside an unsafe block or function?

Answer: Dereferencing a raw pointer (*const T / *mut T).

Raw pointer dereference is one of the operations reserved for unsafe. Safe Rust forbids it because a raw pointer could be null, unaligned, dangling, or aliased, and the borrow checker can’t validate it:

let mut x = 5;
let p: *mut i32 = &mut x;   // create raw pointer (safe)
unsafe { *p = 10; }         // dereference requires unsafe

The other options are safe Rust: Box::new() allocates normally, returning Result from main is supported (the standard pattern), and calling trait-object methods is fully safe. The interview answer: dereferencing raw pointers requires unsafe; everything else listed is safe.

Answer:

Dereferencing a raw pointer (*const T / *mut T).

Raw pointer dereference is one of the operations reserved for unsafe. Safe Rust forbids it because a raw pointer could be null, unaligned, dangling, or aliased, and the borrow checker can’t validate it:

let mut x = 5;
let p: *mut i32 = &mut x;   // create raw pointer (safe)
unsafe { *p = 10; }         // dereference requires unsafe

The other options are safe Rust: Box::new() allocates normally, returning Result from main is supported (the standard pattern), and calling trait-object methods is fully safe. The interview answer: dereferencing raw pointers requires unsafe; everything else listed is safe.

10. What is the difference between raw pointers (*const T, *mut T) and standard references (&T, &mut T)?

Answer: References are guaranteed valid, non-null, borrow-checked; raw pointers bypass lifetimes, may be null, and ignore aliasing rules.

  • References (&T, &mut T): safe pointers the borrow checker validates — always non-null, always point to valid data while in scope, and aliasing rules are enforced (one &mut XOR many &). Created from values, used freely in safe code.
  • Raw pointers (*const T, *mut T): the compiler imposes no guarantees — they can be null, dangling, unaligned, or aliased. Creating them is safe; dereferencing requires unsafe (you manually promise they’re valid).
let mut x = 5;
let r: &mut i32 = &mut x;              // safe, compiler-checked
let p: *mut i32 = &mut x;              // raw pointer
unsafe { *p += 1; }                    // manual safety promise

Raw pointers are for FFI, low-level data structures, and escape hatches. The interview answer: references are borrow-checked, guaranteed-valid pointers; raw pointers carry no compiler guarantees and need unsafe to dereference.

Answer:

References are guaranteed valid, non-null, borrow-checked; raw pointers bypass lifetimes, may be null, and ignore aliasing rules.

  • References (&T, &mut T): safe pointers the borrow checker validates — always non-null, always point to valid data while in scope, and aliasing rules are enforced (one &mut XOR many &). Created from values, used freely in safe code.
  • Raw pointers (*const T, *mut T): the compiler imposes no guarantees — they can be null, dangling, unaligned, or aliased. Creating them is safe; dereferencing requires unsafe (you manually promise they’re valid).
let mut x = 5;
let r: &mut i32 = &mut x;              // safe, compiler-checked
let p: *mut i32 = &mut x;              // raw pointer
unsafe { *p += 1; }                    // manual safety promise

Raw pointers are for FFI, low-level data structures, and escape hatches. The interview answer: references are borrow-checked, guaranteed-valid pointers; raw pointers carry no compiler guarantees and need unsafe to dereference.

11. What is the function of the Drop trait in Rust?

Answer: It provides custom cleanup logic that runs automatically when a value goes out of scope.

Drop is Rust’s destructor trait:

impl Drop for MyResource {
    fn drop(&mut self) {
        // custom cleanup: close files, release handles, log, etc.
    }
}

The compiler calls drop automatically when the value’s scope ends (RAII) — you can’t call it manually (drop(x) is a stdlib function that consumes x, forcing the automatic drop). Because Rust has no garbage collector, this deterministic cleanup is how all resources (memory, files, locks) get released at a predictable point.

String, Vec, Box, File, etc. all implement Drop internally. Types implementing Drop can’t be Copy. The interview answer: Drop lets you define cleanup that runs automatically when a value leaves its scope.

Answer:

It provides custom cleanup logic that runs automatically when a value goes out of scope.

Drop is Rust’s destructor trait:

impl Drop for MyResource {
    fn drop(&mut self) {
        // custom cleanup: close files, release handles, log, etc.
    }
}

The compiler calls drop automatically when the value’s scope ends (RAII) — you can’t call it manually (drop(x) is a stdlib function that consumes x, forcing the automatic drop). Because Rust has no garbage collector, this deterministic cleanup is how all resources (memory, files, locks) get released at a predictable point.

String, Vec, Box, File, etc. all implement Drop internally. Types implementing Drop can’t be Copy. The interview answer: Drop lets you define cleanup that runs automatically when a value leaves its scope.

12. What is the purpose of the std::mem::forget function?

Answer: It consumes a value without running its Drop — an intentional leak or ownership transfer.

std::mem::forget(x) takes ownership of x and makes the compiler skip its destructor:

std::mem::forget(expensive_buffer);   // buffer's memory is never freed

Why ever do this?

  • FFI/ownership transfer: handing a value to C code that will free it later — you must forget it so Rust doesn’t also free it (double-free).
  • Static/global storage: moving a value into a static-like long-lived location where it must never be dropped.
  • Intentional leaks: deliberately keeping a resource alive for the program’s whole lifetime (rare).

It’s the inverse of the default: normally Rust drops everything automatically; forget opts out. Resources thus leaked are reclaimed only when the process exits. The interview answer: forget consumes a value while skipping its Drop, causing an intentional leak/transfer of ownership.

Answer:

It consumes a value without running its Drop — an intentional leak or ownership transfer.

std::mem::forget(x) takes ownership of x and makes the compiler skip its destructor:

std::mem::forget(expensive_buffer);   // buffer's memory is never freed

Why ever do this?

  • FFI/ownership transfer: handing a value to C code that will free it later — you must forget it so Rust doesn’t also free it (double-free).
  • Static/global storage: moving a value into a static-like long-lived location where it must never be dropped.
  • Intentional leaks: deliberately keeping a resource alive for the program’s whole lifetime (rare).

It’s the inverse of the default: normally Rust drops everything automatically; forget opts out. Resources thus leaked are reclaimed only when the process exits. The interview answer: forget consumes a value while skipping its Drop, causing an intentional leak/transfer of ownership.

13. What is the main utility of the std::mem::swap function?

Answer: It swaps the values at two memory locations in place, without violating ownership rules.

std::mem::swap(&mut a, &mut b) exchanges the values of two mutable references:

let mut x = 1;
let mut y = 2;
std::mem::swap(&mut x, &mut y);
// x == 2, y == 1

Why it matters:

  • It moves the values out and back without copying (no Clone needed, works on non-Copy types).
  • It’s the primitive behind many patterns — swap-to-remove from a Vec, RAII guards, temporarily replacing a value, implementing Default-based tricks.
  • It requires two distinct mutable borrows, so ownership rules are preserved.

There’s also std::mem::replace(&mut dest, src) — swap in a value and return the old one. The interview answer: mem::swap exchanges two values in place via mutable references, without copying or moving ownership out.

Answer:

It swaps the values at two memory locations in place, without violating ownership rules.

std::mem::swap(&mut a, &mut b) exchanges the values of two mutable references:

let mut x = 1;
let mut y = 2;
std::mem::swap(&mut x, &mut y);
// x == 2, y == 1

Why it matters:

  • It moves the values out and back without copying (no Clone needed, works on non-Copy types).
  • It’s the primitive behind many patterns — swap-to-remove from a Vec, RAII guards, temporarily replacing a value, implementing Default-based tricks.
  • It requires two distinct mutable borrows, so ownership rules are preserved.

There’s also std::mem::replace(&mut dest, src) — swap in a value and return the old one. The interview answer: mem::swap exchanges two values in place via mutable references, without copying or moving ownership out.

My Private Notes

Notes are auto-saved locally to this device.