Why would you shadow a variable instead of using mut?
Short answer: shadowing (let x = ... again) creates a brand-new binding that happens to reuse
the name — new type, new value, old binding unreachable. mut reuses the same binding, so the type
can never change. When you’re converting a value from one type to another, shadowing is the idiomatic
move; when you’re changing the value of the same thing over time, mut is.
The type change mut cannot do
Section titled “The type change mut cannot do”The classic case: a value starts as a string and ends up as a number. Try it with mut:
fn main() { let mut spaces = " "; spaces = spaces.len();}cargo build prints:
error[E0308]: mismatched types --> src/main.rs:3:14 |2 | let mut spaces = " "; | ----- expected due to this value3 | spaces = spaces.len(); | ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.mut lets you assign a new value, and the compiler checks every assignment against the one type
the binding was declared with. " " made spaces a &str, so assigning a usize is a type
error, no matter how sensible the intent is.
Shadowing is a new binding, so the type can change
Section titled “Shadowing is a new binding, so the type can change”Write let a second time and it compiles:
fn main() { let spaces = " "; let spaces = spaces.len(); println!("spaces = {spaces}");}spaces = 3The second let spaces is a completely separate variable that happens to have the same name. It’s a
usize from birth — there is no assignment to check against the old &str, because the old binding
is simply no longer reachable by this name. Nothing about the first spaces was mutated; it was
replaced.
The place this earns its keep: parsing pipelines
Section titled “The place this earns its keep: parsing pipelines”Converting user input usually takes three steps — trim, parse, use. Without shadowing you either invent a new name per step:
fn main() { let raw = " 3.5 "; let trimmed = raw.trim(); let number: f64 = trimmed.parse().expect("not a number"); println!("{}", number * 2.0);}7or you shadow and keep one name for one concept:
fn main() { let input = " 3.5 "; let input = input.trim(); let input = input.parse::<f64>().unwrap(); println!("{}", input * 2.0);}7Both compile and both print the same thing. The shadowed version reads better once a pipeline grows:
the reader knows input is “the thing we’re processing”, and each line says what happened to it.
raw/trimmed/number force the reader to keep three names in their head for one value — and six
hundred lines later, nobody remembers which of them is current.
There’s a safety side to it too: after let input = input.parse::<f64>(), the old string is
unreachable by name. You cannot accidentally use the un-parsed version further down, because there
is no name left that refers to it.
Shadowing has a scope — mut doesn’t reset
Section titled “Shadowing has a scope — mut doesn’t reset”A shadowed binding lives until the end of its block, and the outer one comes back afterwards:
fn main() { let name = "Ada"; { let name = name.len(); println!("inner: {name}"); } println!("outer: {name}");}inner: 3outer: AdaInside the block, name is the usize. The block ends, the shadow is dropped, and name is the
&str again — the outer binding was never touched. With mut, there is one binding for the whole
scope: once you’ve reassigned it, the old value is gone for good.
That makes the choice concrete. Value changes over time, same type the whole way — mut.
Converting through types, or replacing “the current version” of something — shadow.
Related
Section titled “Related”- Why does Rust require the mut keyword? — the reassignment this article contrasts with
- Next in this chapter: what
constandstaticare, and how they differ from a plainlet - The bits that trip everyone up — the rest of this chapter