Rust Scope
Rust uses lexical, block-based scoping to determine exactly when a variable becomes valid and when it is cleaned up.
What Is a Scope?
A scope is the region of a program where a binding (a variable name) is valid. In Rust, scopes are almost always defined by curly braces `{ }`. A variable becomes valid the moment it is declared with `let`, and it remains valid until the end of the innermost block that contains it. Once the closing brace of that block is reached, the variable goes out of scope and Rust automatically calls `drop` on it if it owns any resources.
Block Expressions
In Rust, a pair of braces `{ }` is itself an expression, called a block expression. Blocks can appear almost anywhere a value is expected: as the body of a function, as an arm of an `if`, or standalone inside another block. The last expression in a block (without a trailing semicolon) becomes the value of the whole block.
A Block as an Expression
fn main() {
let x = 5;
let y = {
let inner = x * 2;
inner + 1 // no semicolon: this is the block's value
};
println!("x = {}, y = {}", x, y);
// `inner` is not accessible here; it went out of scope
}Nested Scopes and Shadowing
Scopes can be nested inside one another, and an inner scope can declare a variable with the same name as one in an outer scope. This is called shadowing. The inner variable temporarily hides the outer one for the rest of the inner block, and the outer variable reappears once the inner block ends. Shadowing is different from mutation: each `let` creates a brand-new binding, potentially with a different type.
Nested Scope with Shadowing
fn main() {
let count = 1;
println!("outer count: {}", count);
{
let count = count + 10; // shadows the outer `count`
println!("inner count: {}", count);
{
let count = "now a string"; // shadowing can change type
println!("innermost count: {}", count);
}
println!("back to inner count: {}", count);
}
println!("back to outer count: {}", count);
}Scope and the Drop Order
When multiple variables are declared in the same block, Rust drops them in reverse order of declaration once the block ends. This matters when values reference or depend on each other, and it is one of the mechanical rules that makes Rust's ownership system predictable rather than magical.
- A variable's scope begins at its `let` declaration, not at the top of the block.
- A variable's scope ends at the closing brace `}` of the innermost enclosing block.
- Function parameters are scoped to the entire function body.
- Values are dropped in the reverse order they were created within a scope.
- Shadowed variables are entirely separate bindings; the old one is simply inaccessible, not destroyed early.
Drop Order Demonstration
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("dropping {}", self.0);
}
}
fn main() {
let _a = Noisy("a");
let _b = Noisy("b");
let _c = Noisy("c");
println!("end of main reached");
// prints: dropping c, dropping b, dropping a
}Scope vs. Lifetime
Scope is a purely lexical concept: it is about where in the source code a name is visible. Lifetime, which you will meet properly when studying borrowing, is about how long a reference remains valid. The two are related — a reference's lifetime can never outlive the scope of the value it points to — but they are not identical, and conflating them is a common source of confusion for newcomers.
Exercise: Rust Scope
When does a variable declared inside a `{ }` block go out of scope?