Skip to content

← Ownership I: Moves, Copy, Clone, Drop step 17 of 20

Easy Primitives

Shadowing is not moving

This exercise compares two pieces of Rust syntax that both reuse a variable name: shadowing and assignment. They look similar but do different things to the value the name held before. You will see the difference in a log that records when each value is created and when it is dropped (destroyed).

pub fn shadow_timeline(scene: i64) -> Vec<String>

scene selects one of five small scenes, numbered 0 to 4, and the function returns that scene's log lines in the order they were recorded. Any other scene value returns an empty vector; the provided match already does this.

Two ways to reuse a name

let t = make("a");
let t = make("b");     // shadowing: a second `let` creates a NEW binding with the same name

let mut u = make("c");
u = make("d");         // assignment: the SAME binding receives a new value

A binding is the variable that a let introduces. Shadowing means declaring a new binding whose name hides an earlier one in the same or an enclosing scope.

Shadowing does not destroy "a". The first binding still exists and still owns its value; the name t simply refers to the second binding from that point on. Both values are dropped when the scope ends, and local variables in one scope are dropped in the reverse of their declaration order, so the log for the code above (with a marker line at the end of the body) is:

new a, new b, end of body, drop b, drop a

Assignment is different. The slot u already holds "c", so before "d" can be stored there, Rust drops the old value. This happens at the assignment, not at the end of the scope.

Check your understanding In u = make("d"), is "c" dropped before or after "d" is built? Show answer

After. The Rust Reference specifies this order for assignment:

  1. evaluate the right-hand side, producing "d"
  2. drop the value currently in u, which is "c"
  3. move "d" into u

So the log reads new d, then drop c. The order matters when the value stands for a resource. If u held a lock guard or an open file, the new resource is acquired before the old one is released.

One consequence is that the right-hand side can fail without leaving u empty. If building "d" panics, u still holds "c", and "c" is dropped once, during unwinding, like any other local.

Two special cases: assigning to a local that has not been initialized yet (let u; u = make("x");), or to one whose value has been moved out, drops nothing at that point, because there is no value in the slot.

E0384: assignment needs mut

Shadowing needs no mut, because each let creates a fresh binding. Assigning to an existing binding requires that binding to be declared let mut. The starter's scene 1 omits mut, and rustc rejects it:

error[E0384]: cannot assign twice to immutable variable `u`
   |
   |     let u = tracked("c".to_string(), log.clone());
   |         - first assignment to `u`
   |     note(log.clone(), format!("holding {}", u.name));
   |     u = tracked("d".to_string(), log.clone());
   |     ^ cannot assign twice to immutable variable
   |
help: consider making this binding mutable
   |
   |     let mut u = tracked("c".to_string(), log.clone());
   |         +++

Fix that error, then write scenes 2, 3 and 4.

The provided helpers

The grader adds these definitions to your program:

type Log = Rc<RefCell<Vec<String>>>;            // a shared, growable list of lines
struct Tracked { name: String, log: Log }
fn tracked(name: String, log: Log) -> Tracked   // pushes "new <name>"
impl Drop for Tracked                           // drop pushes "drop <name>"
fn note(log: Log, line: String)                 // pushes `line` unchanged (a "marker")
fn new_log() -> Log
fn snapshot(log: Log) -> Vec<String>            // copies the lines out

log.clone() copies the Rc handle, not the list, so every clone appends to the same log. You can read a Tracked value's name field, as in u.name.

The five scenes

Every line below is recorded through tracked, note or a drop. Names such as g{i} mean the loop counter formatted into the string, for example format!("g{i}").

0: plain shadowing (provided). Two locals both named _t, holding a and then b, followed by the marker "end of body".

1: assignment. One mut local u holding c. Emit format!("holding {}", u.name). Assign a new Tracked named d to u. Emit format!("holding {}", u.name) again.

2: shadowing in an inner block. A local t holding e; emit format!("outer {}", t.name). Then a block { ... } containing a shadowing local t holding f and the marker format!("inner {}", t.name). After the block, emit format!("back to {}", t.name).

3: shadowing in a loop. For i in 0..2: a local t holding g{i}, the marker format!("first {}", t.name), a shadowing local t holding h{i}, and the marker format!("iter {}", t.name).

4: assignment in a loop. A mut local u holding z0. For i in 1..3: emit format!("holding {}", u.name), then assign a new Tracked named z{i} to u. After the loop, emit format!("final {}", u.name).

Scene 2 shows that shadowing is scoped: after the inner block's closing brace, the name t refers to the outer binding again, and the outer value was never dropped or changed. Scene 3 shows that a loop body is a scope too, so both of that iteration's values are dropped, h{i} first, at the end of every iteration.

Scene 0 names its locals _t rather than t because they are never read, and the leading underscore silences the unused-variable warning. Do not replace them with let _ = ...: the _ pattern binds nothing, so the value is dropped immediately at the end of that statement instead of at the end of the scope.

Clippy and shadowing

Grading compiles your code, runs the tests and runs cargo clippy with -D warnings, which turns every warning into an error.

Clippy's redundant_locals lint, in the default-enabled suspicious group, flags a rebinding such as let x = x; that changes nothing. In the Clippy shipped with Rust 1.98.1, the lint did not fire when the rebinding changed mutability (let mut x = ...; let x = x;) or appeared inside a closure body to make the closure capture x by value, which are the common deliberate uses.

The lints that flag shadowing itself, shadow_same, shadow_reuse and shadow_unrelated, belong to the restriction group, which is off by default. The shadowing in this exercise therefore passes the default Clippy configuration.