5.2Shadowing
Lesson 1.6 cleaned up user input like this, and promised something tidier later:
let trimmed = name.trim();
println!("Hello, {trimmed}!");
It works, but it leaves two variables in scope for one idea: name (the raw input, newline and all) and trimmed (the version you actually want). Nobody should use name again after the trim, yet there it sits, available, one autocomplete away from a bug. Later is now; here's the tidier idiom:
let name = name.trim();
println!("Hello, {name}!");
A second let with the same name. This is called shadowing: the new variable shadows the old one, and from this line on, the name name refers to the new variable. The raw, untrimmed original hasn't been changed or destroyed; it's merely unreachable, standing politely behind its replacement. That's exactly the property we wanted: the dangerous untrimmed version can no longer be touched, by construction.
Shadowing is not mutation
This looks like it breaks lesson 1.4's immutability rules, so let's be precise about why it doesn't. Compare:
fn main() {
let x = 5;
x = x + 1; // assignment to an immutable variable
println!("{x}");
}
That's the familiar E0384 (cannot assign twice to immutable variable), with the compiler suggesting mut. But:
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("{x}");
}12
This compiles without complaint, because nothing was ever assigned to. Three variables were created; each happened to be named x; each let computed its value from the one before and then stepped in front of it. Mutation changes the contents of one box. Shadowing brings a new box and hangs the old label on it.
The distinction stops being philosophical at a block boundary. Lesson 1.11 showed that blocks nest scopes, and a shadow only reaches as far as its scope:
fn main() {
let x = 1;
{
let x = 10;
println!("inner: {x}");
}
println!("outer: {x}");
}inner: 10
outer: 1
Inside the block, the inner x shadows the outer. When the block ends, the inner x is gone and the original steps back out, value intact, exactly as it would have if the inner variable had been named y. Had line 4 been a real assignment to a mut x, the second print would say 10. That's the test: mutation survives the block, shadowing doesn't.
Warning
That same property makes a classic trap. Write let x = 10; inside a block when you meant to assign to the outer x, and the program compiles, runs, and quietly ignores your update at the end of the block. If a value seems to "reset itself" after a block, look for an accidental let.
The shadow can change type
Here's what mut can't do. A mutable variable is still one box, with one type, fixed at birth:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
println!("{spaces}");
}error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
The variable holds text; you can't assign a number into it, however mutable it is. Shadowing has no such restriction, because each let is a brand-new variable that's free to have a brand-new type:
fn main() {
let spaces = " ";
let spaces = spaces.len();
println!("{spaces}");
}3
The first spaces is text; the second is a number (usize, text lengths being memory measurements, per lesson 4.3); the idea stays "the spaces, in whatever form I currently need." One name tracking one concept across changing types is what shadowing is for, and it's why the idiom is everywhere in real Rust.
It's also quietly present in let name = name.trim(); from the top of this lesson. The original name is a String; the trimmed shadow is something else (the compiler error in that mut spaces transcript already leaked its name: &str). What that type is, and why trimming produces it, is precisely the next two lessons.
Quiz time
Question #1
What does this print?
fn main() {
let score = 10;
let score = score + 5;
{
let score = 100;
println!("{score}");
}
println!("{score}");
}Show solution
100
15
The middle let makes the function-level score 15 (shadowing the original 10). The block's score is 100 only inside the block; afterward the 15 steps back out. Nothing here needed mut, and nothing was mutated.
Question #2
Predict the compiler's reaction:
fn main() {
let mut answer = 42;
answer = "forty-two";
println!("{answer}");
}
Then state the one-keyword-sized change that makes it compile.
Show solution
error[E0308]: mismatched types: the assignment tries to put a &str into a variable whose type is integer ("expected integer, found &str", with the let line flagged as "expected due to this value"). mut permits new values, never new types.
Change answer = to let answer = (and the now-pointless mut should go too): a shadow may be any type it likes, so the program compiles and prints forty-two.
Question #3
This compiles and works. Rewrite it with shadowing so that exactly one variable name appears in the function, and explain what got safer:
use std::io;
fn main() {
println!("What's your favorite word?");
let mut raw = String::new();
io::stdin()
.read_line(&mut raw)
.expect("failed to read input");
let cleaned = raw.trim();
println!("{cleaned} has {} letters.", cleaned.len());
}Show solution
use std::io;
fn main() {
println!("What's your favorite word?");
let mut word = String::new();
io::stdin()
.read_line(&mut word)
.expect("failed to read input");
let word = word.trim();
println!("{word} has {} letters.", word.len());
}
One concept, one name. The safety gain: after the shadow, the raw newline-bearing input is unreachable, so no later line can accidentally use the uncleaned version. (With raw and cleaned both in scope, that mistake stays available forever. Also, len() counting bytes rather than letters is a wrinkle the next lesson confesses to properly.)
You've now twice brushed against the mystery type that trim produces. Next lesson: the text type you've been creating since lesson 1.6. The lesson after: the one you've been creating since lesson 0.8 without knowing it.