Drop and Resource Management

Variable Scope and Destruction

A variable's drop scope is the lexical region at whose exit the compiler runs the variable's destructor. Understanding how nested blocks, temporary expressions, let _ = ... wildcards, and temporary lifetime extension affect destruction timing is essential when managing locks and scoped guards.

Lexical Block Scopes and Shadowing

  1. Nested blocks ({ ... }): A variable declared inside an inner { ... } block is dropped immediately at the closing brace } of that block.
  2. Variable shadowing (let x = ...; let x = ...;): Re-declaring a variable with the same name (let x = ...) does not immediately drop the original binding! Both bindings remain alive in the current scope and are dropped in reverse declaration order when the enclosing block ends.
  3. Reassignment (x = new_val;): Assigning a new value to an existing mutable binding (mut x) does immediately drop the old value held in x before storing new_val.
struct Token(&'static str);

impl Drop for Token {
    fn drop(&mut self) {
        println!("dropped {}", self.0);
    }
}

fn main() {
    println!("--- Shadowing keeps old binding alive until block end ---");
    {
        let x = Token("x1 (shadowed)");
        let x = Token("x2 (shadowing)");
        println!("active x is {}", x.0);
    }

    println!("--- Reassignment drops old value immediately ---");
    {
        let mut y = Token("y1 (initial)");
        println!("before reassignment");
        y = Token("y2 (replacement)");
        println!("after reassignment, active y is {}", y.0);
    }
}
--- Shadowing keeps old binding alive until block end ---
active x is x2 (shadowing)
dropped x2 (shadowing)
dropped x1 (shadowed)
--- Reassignment drops old value immediately ---
before reassignment
dropped y1 (initial)
after reassignment, active y is y2 (replacement)
dropped y2 (replacement)

The Wildcard Trap: let _ = expr vs. let _guard = expr

The wildcard pattern _ does not bind a value to a variable; let _ = expr; evaluates expr and drops the result immediately at the semicolon. In contrast, a named variable prefixed with an underscore (let _guard = expr;) binds the value to a real local variable that lives until the end of the enclosing block.

let _ = acquire_guard();        // Dropped immediately at `;` on this line!
perform_critical_work();        // Runs UNPROTECTED!

let _guard = acquire_guard();   // Bound to `_guard`; stays alive until `}`!
perform_critical_work();        // Runs while `_guard` is active!
struct ScopeTimer(&'static str);

impl Drop for ScopeTimer {
    fn drop(&mut self) {
        println!("timer {:?} finished", self.0);
    }
}

fn main() {
    let _ = ScopeTimer("wildcard (drops immediately)");
    let _named = ScopeTimer("named _named (drops at end of main)");
    println!("executing body of main...");
}
timer "wildcard (drops immediately)" finished
executing body of main...
timer "named _named (drops at end of main)" finished

Temporary Scopes and Temporary Lifetime Extension

  • Temporary values: An unnamed intermediate value created inside a statement (such as Mutex::new(0).lock().unwrap().to_string();) lives until the end of the enclosing statement (the terminating ;), unless it is inside the scrutinee of an if let or match expression where it can remain alive for the matched arm.
  • Temporary lifetime extension: When a let statement borrows directly from a temporary R-value expression (let r = &vec![1, 2, 3];), the compiler extends the lifetime of the temporary Vec<i32> to match the enclosing block scope of r.