December Code

Rust ownership interview questions, with answers

Ownership is the idea that makes Rust different from every language most candidates already know, and it is the first thing Rust interviews test. Every value has one owner, ownership can be moved, and the value is freed when its owner goes out of scope. From those rules follow Rust's memory safety without a garbage collector, and also the compile errors that surprise newcomers.

The answers below work through the rules, what a move is, how Copy and Clone differ, how values are dropped and captured by closures, with code compiled and run on rustc 1.98. Then take the free Rust diagnostic — ten questions across every Rust topic in the bank — to see which of these you can explain but not yet predict.

The questions, with answers

  1. 1.What is ownership in Rust?

    In short: Every value has exactly one owner, ownership can be transferred, and when the owner goes out of scope the value is dropped and its memory freed.

    Rust manages memory through three rules. Each value has a variable that owns it. There is only one owner at a time. When the owner goes out of scope, the value is dropped, which runs its cleanup and frees any heap memory it holds. Because the compiler knows statically where every owner's scope ends, it inserts the cleanup itself, with no garbage collector and no manual free. Borrowing lets code use a value without taking ownership, and moving transfers ownership to a new variable or function. Most of Rust's compile-time errors about lifetimes and moved values are these rules being enforced.

    let name = String::from("ann");
    let len = name.len();
    println!("{name} {len}");
    // ann 3
  2. 2.What is a move in Rust?

    In short: A move transfers ownership of a value to another variable or function; the old variable can no longer be used, so the value is freed exactly once.

    Assigning a value that owns heap data, or passing it to a function by value, moves it: the bits of the handle are copied, but ownership passes to the new place and the old variable is treated as uninitialised. Calling eat(v) below moves the vector into eat, which owns it from then on and drops it when it returns; any later use of v in main would be a compile error. Moves are cheap, because only the pointer, length and capacity are copied, never the elements. When the caller needs the value afterwards, it should pass a reference instead, &v, or clone the value.

    fn eat(v: Vec<i32>) -> usize {
        v.len()
    }
    
    fn main() {
        let v = vec![1, 2, 3];
        let n = eat(v);
        println!("{n}");
        // 3
    }
  3. 3.What is the difference between Copy and Clone in Rust?

    In short: Copy types are duplicated implicitly by a bit-for-bit copy on assignment; Clone is an explicit .clone() call that can do arbitrary work, such as copying heap data.

    Clone is a trait with a clone method that makes a duplicate, which for a String or a Vec means allocating and copying its contents; it only happens when you call it. Copy is a marker trait for types whose duplicate is just a copy of their bytes, such as integers, floats, bool, char, and tuples or structs made only of Copy types. For a Copy type, assignment and passing by value copy instead of moving, so the original stays usable, which is why a and b below both work. A type that owns heap memory or implements Drop can never be Copy. Every Copy type must also implement Clone.

    #[derive(Clone, Copy)]
    struct Pt {
        x: i32,
    }
    
    fn main() {
        let a = Pt { x: 1 };
        let b = a;
        let s = String::from("ok");
        let t = s.clone();
        let (p, q) = (a.x, b.x);
        println!("{p} {q}");
        // 1 1
        println!("{s} {t}");
        // ok ok
    }
  4. 4.How do you drop a value early in Rust?

    In short: Call std::mem::drop(value), which takes ownership and lets the value go out of scope at once; calling the Drop trait's method directly is not allowed.

    Values are normally dropped at the end of their owner's scope, but sometimes a resource should be released sooner, such as a lock guard or a large buffer. drop(x), from the prelude, is simply a function that takes its argument by value and does nothing with it, so the value moves into drop and is dropped when drop returns. After that, x has been moved and cannot be used. Calling x.drop() yourself is a compile error, because the compiler would then run the destructor a second time at the end of the scope. Scoping with an inner block achieves the same effect.

    let data = vec![0u8; 1024];
    let n = data.len();
    drop(data);
    println!("{n}");
    // 1024
  5. 5.How does Rust manage memory without a garbage collector?

    In short: The compiler tracks every value's owner and inserts the code to free it where the owner's scope ends, so memory is released deterministically with no run-time collector.

    A garbage collector finds unreachable memory at run time. Rust instead proves at compile time when each value stops being used: ownership gives every value exactly one owner, and the borrow checker ensures that no reference outlives the value it points to. With that information, the compiler emits a call to each value's destructor at the end of its owner's scope, the pattern C++ calls RAII, so memory, file handles and locks are released at a predictable point. The costs are the rules themselves and occasional reference counting through Rc or Arc when ownership really is shared, which is explicit in the types.

  6. 6.Why can't you move a value out of a Vec by index in Rust?

    In short: Moving out would leave a hole the Vec still owns, so v[i] on a non-Copy element is a compile error; clone it, borrow it, or use remove, swap_remove or into_iter.

    v[0] is a place inside the vector, not a value you can take: moving a String out of it would leave the vector owning an element that had already been moved, and it would be freed twice. So let s = v[0]; is rejected for non-Copy element types, with error E0507, cannot move out of index of Vec<String>. The options depend on intent: borrow it with &v[0], copy it with v[0].clone(), take it out and shift the rest with v.remove(0), or consume the whole vector with into_iter to own each element. For Copy types such as integers, v[0] simply copies the value.

    let v = vec!["a".to_string()];
    let s = v[0];
    // does not compile
    let t = v[0].clone();
    println!("{t}");
    // a
  7. 7.What are move closures in Rust?

    In short: A closure marked move takes ownership of the variables it uses instead of borrowing them, so it can outlive the scope that created it, as a thread must.

    By default a closure captures each variable in the least demanding way its body allows: by shared reference, by mutable reference, or by value if the body consumes it. The move keyword forces capture by value, so the closure owns the captured data. That is required when the closure may run after the current function returns, such as the body of thread::spawn or a returned iterator adapter, because a borrowed variable would no longer exist. After a move closure captures msg below, msg itself can no longer be used, but f can still read it through its own copy. Copy types are copied into a move closure rather than moved.

    let msg = String::from("hey");
    let f = move || msg.len();
    println!("{}", f());
    // 3

How the diagnostic asks it

One question from the Rust bank, exactly as a sitting would show it. The bank has 4 on ownership & moves and 30 across Rust.

Ownership & Moves · mediumRUST-006

What does this Rust code print?

let a = 5;
let b = a;
let v = vec![1, 2];
let w = v;
println!("{} {}", a, b);
println!("{:?}", w);
  1. 15 5, then [1, 2]correct
  2. 2It does not compile: a was moved into b
  3. 3It does not compile: v was moved into w
  4. 45 0, then [1, 2]

Simple scalar types such as i32 implement the Copy trait, so let b = a copies the bits and both a and b are 5. A Vec owns a heap buffer and is not Copy, so let w = v moves it: v becomes unusable. But v is never used again, and a move is only an error when the old name is used after it, so the program compiles and prints 5 5, then [1, 2]. The option about a assumes every assignment moves. The option about v treats the move itself as the error. 5 0 imagines that a copied-from integer is reset, which never happens.

Measure it

Reading answers tells you what’s true. A diagnostic tells you what you get wrong.

10 Rust questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.

What the readiness test measures · how the score is computed

By Harshit · updated