2.4Local scope and blocks
Functions gave your programs rooms. This lesson is about which names are visible from which room, a concept called scope, and it answers a question you may already have wondered about: what happens when two functions both want a variable named x?
Scope
A variable's scope is the region of code where its name can be used, and the rule in Rust is satisfyingly mechanical: a variable's scope runs from its let to the closing brace } of the block it was declared in. Before the let, the name doesn't exist yet. After the brace, it's gone.
fn main() {
let outer = 1;
{
let inner = 2;
println!("{outer} {inner}");
}
println!("{outer}");
}1 2
1
The inner block can see outer (it's still before outer's closing brace), so both print. But flip it around and try to use inner after its block has closed:
fn main() {
{
let inner = 2;
}
println!("{inner}");
}error[E0425]: cannot find value `inner` in this scope
--> src/main.rs:5:16
|
5 | println!("{inner}");
| ^^^^^
|
help: the binding `inner` is available in a different scope in the same function
--> src/main.rs:3:13
|
3 | let inner = 2;
| ^^^^^
(Note the compiler even shows you where the binding does exist, in case the brace placement was the surprise.)
As far as line 5 is concerned, inner never existed. Scope is enforced at compile time; the compiler isn't checking whether the value is gone, it's checking whether the name is visible from here. (What happens to the value itself when its scope ends is a deeper story with its own chapter; lesson 8.3 introduces the word "dropped" properly. For the integers we're using, nothing interesting happens; the box is just reclaimed.)
A variable that lives inside a function (including the parameters) is a local variable, local to that function. Which sets up the answer to the opening question.
Functions don't share variables
Each function's variables are its own. The same name in two functions means two completely unrelated variables:
fn main() {
let x = 1;
let y = 2;
println!("main: {x} {y}");
swap_demo(x);
println!("main again: {x} {y}");
}
fn swap_demo(y: i32) {
let x = 100;
println!("swap_demo: {x} {y}");
}main: 1 2
swap_demo: 100 1
main again: 1 2
Read the middle line carefully: inside swap_demo, x is its own local 100, and y is its parameter, which received a copy of main's x (so, 1). None of it touches main's variables, as the last line proves. The names match by coincidence; the variables never met.
Key insight
This isolation is a feature you'll lean on constantly: when writing a function, you can name things whatever reads best inside that function, without checking what every other function named anything. Functions are sealed rooms, and the only doors are parameters in and return values out. (Chapters 8 and 9 are entirely about what passes through those doors; the doors themselves never change.)
Where to define your variables
Since a variable exists from its let to its block's end, you choose its scope by choosing where to write the let. Old C tradition declared everything at the top of the function; modern practice (and Rust idiom) says otherwise:
Best practice
Define each variable as close to its first use as you can. A variable born three lines before it's needed is three lines of "wait, what's this for?"; a variable born at first use explains itself. Bonus: shorter scopes mean fewer live names to track at any point in the function, for you and for the compiler's more advanced checks alike.
And the 1.11 connection completes itself: blocks are expressions, so a block can both scope some scratch work and produce its result, leaving no temporary names behind:
fn main() {
let minutes = 200;
let formatted = {
let h = minutes / 60;
let m = minutes % 60;
format!("{h}h {m}m")
};
println!("{formatted}");
}3h 20m
h and m exist exactly as long as they're useful and not a line longer. (format! is println!'s sibling that produces the text instead of printing it; lesson 5.5 covers it. And % is the remainder operator, formally lesson 6.2.)
Quiz time
Question #1
What does this program print? Work it out before running it; this is the chapter's central trace.
fn main() {
let a = 5;
let b = 10;
print_things(b);
println!("main: {a} {b}");
}
fn print_things(a: i32) {
let b = a + 1;
println!("print_things: {a} {b}");
}Show solution
print_things: 10 11
main: 5 10
print_things's parameter a receives a copy of main's b (10), and its local b is 11. Main's a and b are different variables that never changed.
Question #2
Predict the compiler error:
fn main() {
let total = {
let price = 40;
price * 2
};
println!("{total} from price {price}");
}Show solution
E0425: cannot find value price in this scope, at the final println!. price died at the inner block's closing brace; only total (80) survived. Printing just {total} fixes it.
Question #3
True or false: a function can read its caller's local variables if it knows their names.
Show solution
False, and it's one of the load-bearing falsehoods of the whole language. A function sees its own locals and parameters, nothing else. If a function needs a caller's value, the caller passes it as an argument.
You now know how functions work mechanically. The next two lessons are about using them well: first why they're worth the ceremony, then how to design a program as a team of them.