Nothing checks a shadow¶
Level: 201 · working knowledge
One line: rustc has no lint for a shadowed variable — what gets mistaken for protection is the type error a wrong shadow trips on its way past, so when the shadow's type matches what it hides there is nothing between you and a wrong answer.
Three pages already cover shadowing here: Shadowing and unwrap on what it is for, A shadow does not drop on what it does to the value underneath, and When to shadow on whether to reach for it at all — with SHADOWING.md as the map of the set. The first two are about the mechanism; the third is a judgement call. This one asks something narrower: what tooling stands between you and a shadow that is simply wrong. The reassuring answer is in wide circulation and it is not true:
"Most downsides of shadowing are negated by the borrow checker. If you got confused around variables, that code is probably not going to compile."
It compiles.
The shadow that compiles clean¶
let scores = [5u32, 3, 0, 4];
let total = 0;
for s in scores {
let total = total + s;
println!(" running total: {total}"); // 5, then 3, then 0, then 4
}
println!(" final total: {total}"); // 0
let inside the loop body makes a new total on every pass, built from the outer one — which is still 0, because nothing ever writes to it. Each iteration computes one score, prints it, and drops it at the closing brace.
The running log is the part worth staring at. It is not obviously broken; it is four plausible numbers in a column, and they happen to be the inputs rather than a sum. A four-element example makes that visible. A real one with two hundred rows does not.
rustc compiles this with no output at all.
Why nothing warned¶
There is no shadowing lint in the compiler. rustc -W help mentions "shadow" four times and every one is about trait items, Deref supertraits, or glob re-exports — none about a let.
Two things come close, and the important thing about both is that neither is a shadow check:
unused_variables, but only if the shadow is never read. Delete theprintln!from that loop and the lint arrives. Keep any use of the value at all — a log line, a comparison, a push into a vector — and it has nothing to say. The bug survives precisely because the code around it looks like working code.- A type mismatch downstream, if the shadow's type differs from what later lines expect.
E0308: mismatched types.
The second one is the source of the folklore. Most shadows people write do change the type — that is what the feature is for — so most mistaken shadows change the type too, and get caught. The catch is a side effect of the type system doing its ordinary job, and it is unrelated to shadowing. Take the type change away and it goes with it.
Which gives the rule worth carrying: a shadow whose type differs from what it hides is checked; a shadow whose type matches is not. Same-type shadows are almost all of the dangerous ones, because a same-type shadow is nearly always a mistake — if you genuinely wanted a second u32, you wanted a second name.
The three lints that do see it — and what the useful one costs¶
Clippy has three, and running each of them against the kata file — which contains one accumulator bug and one perfectly correct parse chain — separates them cleanly:
$ cargo clippy -- -W clippy::shadow_same # finds nothing
$ cargo clippy -- -W clippy::shadow_unrelated # finds nothing
$ cargo clippy -- -W clippy::shadow_reuse
warning: `totals` is shadowed
--> src/main.rs:36:13 <- the bug
warning: `raw` is shadowed
--> src/main.rs:78:9 <- correct code
warning: `raw` is shadowed
--> src/main.rs:79:9 <- correct code
| Lint | Fires on | On the bug? |
|---|---|---|
shadow_same ↗ |
let x = x;, let x = &x; |
no — it only ever catches junk |
shadow_unrelated ↗ |
let x = something_else; |
no — the accumulator reuses total, so this lint is silent |
shadow_reuse ↗ |
let x = f(x); |
yes — and on let x = x.trim().parse()? too |
The lint that catches the bug is the one that also condemns the idiom. shadow_reuse cannot tell let total = total + s inside a loop from let raw: u32 = raw.parse()? at the top of a function, because syntactically they are the same move — and the second is what chapter 3.1 of the Book teaches on the page where it introduces the feature.
All three are allow-by-default restriction lints, which is clippy saying the same thing in its own vocabulary: restriction is the group for "you may want to forbid this, and we are not claiming it is wrong." So enabling shadow_reuse is a house-style commitment — plausible for a large team, or a codebase that is mostly loops — and not a bug filter you switch on and forget. Enabling it to catch one accumulator will cost you every honest parse chain in the crate.
If you want one of them on, shadow_unrelated is the cheapest, because let config = other_thing; under an existing config is hard to justify. It just will not catch this.
A historical note: the one it did catch was not a shadow¶
Worth knowing before you turn shadow_unrelated on, because it is a good illustration of how thin the tooling's grasp of this topic has been. For two years the recommended lint fired on code containing no let at all:
error: `y` shadows a previous, unrelated binding
|
6 | (x, y) = (3, 4);
| ^
note: previous binding is here
|
6 | (x, y) = (3, 4);
| ^
Read the two spans: the "previous binding" it named is x, on the same line, in the same tuple. Nothing there is a shadow — (x, y) = (3, 4) writes into two existing places, which is precisely the operation a shadow is not. The lint had confused an assignment with a declaration, which is the same conflation the reader is here to unlearn, running inside the tool sold as the cure for it.
It was reported in February 2023, was still reproducing when it came up on the users forum in January 2025, and was fixed by rust-clippy#14381 ↗, merged on 2025-03-27. It is genuinely gone — on clippy 0.1.97 that block passes under #[deny(clippy::shadow_unrelated)], while a real unrelated shadow in the same file still stops the build, so the lint is live and simply no longer wrong about this.
The reason to keep the story rather than delete it: this page's argument is that almost nothing stands between you and a bad shadow. The tooling that does exist is three allow-by-default lints, the only one that catches the accumulator also bans the Book's own idiom, and the one recommended as cheapest spent two years unable to tell a write from a declaration. That is the actual state of the net, and it is worth knowing how recently the last hole was patched. See A name is not a place for the distinction the lint was missing.
Items are shadowed too¶
fn and let share the value namespace, so shadowing is not only about variables — and the two directions get opposite treatment.
A fn inside a block shadows one outside it, in total silence:
fn threshold() -> u32 { 50 }
fn main() {
println!("{}", threshold()); // 50
{
fn threshold() -> u32 { 5 }
println!("{}", threshold()); // 5 — a different function, no warning
}
println!("{}", threshold()); // 50
}
A let shadowing a fn is caught, but only because calling a u32 is a type error rather than a naming one:
error[E0618]: expected function, found `u32`
--> fnshadow.rs:6:20
|
1 | fn seats() -> u32 { 5 }
| ----------------- this function of the same name is available here, but it's shadowed by the local binding
...
4 | let seats = 3u32;
| ----- `seats` has type `u32`
6 | println!("{}", seats());
| ^^^^^-- call expression requires function
Note rustc's own word for it in that note: shadowed. Which settles a question people argue about, below.
The same shape, where the compiler does stop you¶
Put an owned value through the identical shadow-in-a-loop and it becomes a hard error:
let name = String::from("Ada");
let mut jobs: Vec<Box<dyn Fn()>> = Vec::new();
for i in 1..=3 {
let name = name.clone(); // delete this line and it stops compiling
jobs.push(Box::new(move || println!("job {i}: {name}")));
}
error[E0382]: borrow of moved value: `name`
| value moved into closure here, in previous iteration of loop
help: consider cloning the value before moving it into the closure
That let name = name.clone(); before a move closure is the most common shadow in real Rust — it is in every codebase that spawns tasks — and it is one of the few where forgetting it is caught.
The contrast is the whole page in miniature. Structurally these are the same construct: a let in a loop body, same name as something outside. One is a wrong answer with no diagnostic; the other will not build. The difference is not the shadow, it is what got lost. Losing a number breaks no rule the compiler knows. Losing a String twice over breaks the only rule it cares about most.
(Worth noticing in that help: — rustc suggests let value = name.clone(), a different name. The compiler's own advice is the non-shadowing form; humans write the shadow because it saves inventing name2.)
Why people disagree about whether this is even shadowing¶
A surprising amount of argument about shadowing is two definitions passing each other, and it matters here because one of them makes the dangerous case invisible.
The narrow definition — a binding in an inner scope hiding one in an outer scope — is the textbook one, and under it nearly every language shadows: C, C++, Java, Python, JavaScript, Lisp, Perl all let an inner scope reuse a name. Nothing about it is Rust-specific, and nothing about it is interesting.
The wide definition — including a second let of the same name in the same scope — is the one Rust uses. The Book's chapter is titled "Shadowing" and its examples are same-scope. Clippy names its lints shadow_* for exactly those cases, as the transcript above shows. rustc's E0618 note says "shadowed" about a local hiding an item.
The unusual part of Rust is the second one, and it is also where the silent bug lives — the accumulator above is a same-scope shadow inside a loop body. So "shadowing is fine, every language has it" is true of the narrow definition and irrelevant to the risky one, while "that isn't shadowing, that's rebinding" gets the mechanism backwards: let total = total + s creates a genuinely new variable, and the old one is still alive, not overwritten.
If you are coming from another language¶
- Python.
total = total + sinside a loop simply works — one name, rebound, no new variable. Python cannot have this bug in this shape, and its nearest equivalent runs the other way: assign to a name you meant to read from an enclosing scope and you getUnboundLocalErrorat run time, which at least is an error. What transfers is the habit of a loop accumulator being a single mutable name; what changes is that Rust offers you a second way to write that line which looks identical and silently does not accumulate. - ABAP. There is no shadowing at all — a
DATAname is one typed variable for the whole routine, so the compiler resolveslv_totalto the same storage everywhere and this bug is unwriteable. The familiar shape is a misplacedCLEAR lv_totalinside theLOOPrather than before it: syntax check clean, ATC quiet, and the total comes out as the last row. Same lesson, different mechanism — an accumulator reset once per pass, and no tool that considers it its business. - C and C++. Both refuse a same-block redeclaration outright, so the risky form is unwriteable, and both offer
-Wshadow(off by default) for the nested-scope kind. That is the more interesting comparison: C treats shadowing as a suspected mistake and gives you one switch, Rust treats it as an idiom and gives you three, all off. The sibling page walks through what C and C++ do with the same program. - Java. Often said not to allow shadowing; the claim is about locals only. Java forbids redeclaring a local inside a nested block of the same method, but JLS §6.4.1 is literally titled "Shadowing" and a parameter shadowing a field is the most common pattern in the language — it is why constructors say
this.x = x. What Java lacks is the same-scope form, which is the one this page is about.
Practice¶
The total that never totalled. Write a small point count: three or four rounds of 0–5 points over three players, an accumulator outside the loop, a per-round log line inside it, and a leader function that returns the highest-scoring player. Introduce the bug from the top of this page — let totals = … inside the loop — and run it.
Read the output before reading the code. Decide whether you would have caught it in review.
Then go looking for a tool that catches it: build it, read every warning, and run each of clippy's three shadow lints in turn. One of them finds it. Work out what else that one flags in the same file, and decide whether you would turn it on.
Then fix it three ways and pick one to ship.
Worth getting wrong on purpose: delete the log line from inside the loop, rebuild, and watch a warning appear that was not there a moment ago — then put the line back and watch it leave. That is the whole margin you were relying on.
Solution
nothing_checks_a_shadow_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: the total that never totalled.
//!
//! A shadowed accumulator inside a loop. The compiler says nothing, the
//! per-round log looks fine, the report prints a real player's name — and
//! the number behind that name is zero. Then: which of clippy's three shadow
//! lints finds it, and what that one costs you elsewhere in this same file.
//!
//! rustc --edition 2024 nothing_checks_a_shadow_kata.rs -o /tmp/ncask && /tmp/ncask
const PLAYERS: [&str; 3] = ["Ada", "Ben", "Cara"];
/// Three rounds, 0–5 points each. Ben is the honest leader: 3 + 5 + 2 = 10,
/// against Ada's 9 and Cara's 6.
const ROUNDS: [[u32; 3]; 3] = [[5, 3, 0], [4, 5, 1], [0, 2, 5]];
fn banner(title: &str) {
println!("\n──── {title}");
}
fn leader(totals: [u32; 3]) -> &'static str {
let mut best = 0;
for i in 1..totals.len() {
if totals[i] > totals[best] {
best = i;
}
}
PLAYERS[best]
}
// ─────────────────────────────────────────────────────────────── the bug
fn total_buggy() -> [u32; 3] {
let totals = [0u32; 3];
for round in ROUNDS {
// Reads the OUTER `totals` — which is still [0, 0, 0] — adds one
// round, and drops the result at the closing brace.
let totals = [
totals[0] + round[0],
totals[1] + round[1],
totals[2] + round[2],
];
println!(" counted {round:?} running total {totals:?}");
}
totals
}
// ─────────────────────────────────────────────────────────────── the fixes
fn total_mut() -> [u32; 3] {
let mut totals = [0u32; 3];
for round in ROUNDS {
for i in 0..totals.len() {
totals[i] += round[i];
}
}
totals
}
fn total_fold() -> [u32; 3] {
ROUNDS.iter().fold([0u32; 3], |mut acc, round| {
for i in 0..acc.len() {
acc[i] += round[i];
}
acc
})
}
fn total_distinct_names() -> [u32; 3] {
let mut running = [0u32; 3];
for round in ROUNDS {
for i in 0..running.len() {
running[i] += round[i];
}
}
running
}
// A CORRECT shadow, in the same file, for the lint to have an opinion about.
fn parse_limit(raw: &str) -> u32 {
let raw = raw.trim(); // &str -> &str
let raw: u32 = raw.parse().unwrap_or(0); // &str -> u32, the Book's own idiom
raw
}
fn main() {
// ────────────────────────────────────────────────────────── 1
banner("As shipped: a log that looks fine and a leader that is not");
let totals = total_buggy();
println!(" Leader: {}", leader(totals));
println!(" Nothing above looks alarming. Every round was counted, each");
println!(" running total is a real number, and Ada is a real player.");
println!(" Two things give it away, and only in hindsight:");
println!(" totals = {totals:?}");
println!(" Every 'running total' was just that round echoed back, and");
println!(" the accumulator never left zero. `leader` broke the all-zero");
println!(" tie by index, so the report named whoever was first in the");
println!(" player list. The honest leader is Ben, with 10.");
// ────────────────────────────────────────────────────────── 2
banner("What the compiler had to say about it: nothing");
println!(" $ rustc --edition 2024 nothing_checks_a_shadow_kata.rs");
println!(" $ <- no output, exit 0");
println!(" `unused_variables` cannot fire: the shadow is read on the");
println!(" next line, by the log. There is no type error: both are");
println!(" [u32; 3]. And there is no shadowing lint in rustc to fire in");
println!(" the first place. Three near-misses, all accidents of shape —");
println!(" drop the log line and the first one would have caught it.");
// ────────────────────────────────────────────────────────── 3
banner("Which clippy lint finds it (recorded from a real run on this file)");
println!(" (clippy needs a cargo crate, so this file is src/main.rs there)");
println!(" $ cargo clippy -- -W clippy::shadow_same");
println!(" $ <- finds NOTHING");
println!();
println!(" $ cargo clippy -- -W clippy::shadow_unrelated");
println!(" $ <- finds NOTHING");
println!();
println!(" $ cargo clippy -- -W clippy::shadow_reuse");
println!(" warning: `totals` is shadowed");
println!(" --> src/main.rs:36:13 <- the bug");
println!(" warning: `raw` is shadowed");
println!(" --> src/main.rs:78:9 <- parse_limit, line 1");
println!(" warning: `raw` is shadowed");
println!(" --> src/main.rs:79:9 <- parse_limit, line 2");
println!(" parse_limit(\" 42 \") = {}", parse_limit(" 42 "));
println!(" ...which is correct code, flagged twice. It is the idiom");
println!(" chapter 3.1 of the Book teaches.");
// ────────────────────────────────────────────────────────── 4
banner("The trade, stated plainly");
println!(" shadow_same `let x = x;` junk always, the bug never");
println!(" shadow_unrelated `let x = something_else;` silent here");
println!(" shadow_reuse `let x = f(x);` catches it — and the idiom");
println!(" The only lint that would have caught this bug is the one that");
println!(" also condemns the good use of the feature. All three are");
println!(" allow-by-default `restriction` lints, which is clippy saying");
println!(" the same thing in its own vocabulary: a style commitment, not");
println!(" a bug filter. Turn `shadow_reuse` on and you have banned");
println!(" `let x = x.trim().parse()?` across the crate. That can be a");
println!(" trade worth making — a large team, a codebase full of loops —");
println!(" but make it deliberately, not hoping to catch one accumulator.");
// ────────────────────────────────────────────────────────── 5
banner("Three fixes, and the one to ship");
let (a, b, c) = (total_mut(), total_fold(), total_distinct_names());
println!(" 1. `mut`, no shadow -> {a:?} leader {}", leader(a));
println!(" 2. fold, no accumulator -> {b:?} leader {}", leader(b));
println!(" 3. no name reused -> {c:?} leader {}", leader(c));
println!(" Fix 2 is the one to ship, and the reason generalises past this");
println!(" bug: it removes the BINDING, so the mistake has nowhere to");
println!(" live. Fix 1 works, and is the honest counter-example to");
println!(" 'shadowing lets you keep everything immutable' — an");
println!(" accumulator is supposed to survive the iteration, so a fresh");
println!(" binding per pass is precisely the wrong tool. Fix 3 works and");
println!(" relies on you not reusing a name, which is the discipline that");
println!(" just failed.");
println!("\n The through-line: the compiler polices ownership, types and");
println!(" exhaustiveness, and a shadow can be wrong in none of those");
println!(" ways. When a shadow's type matches what it hides, you are the");
println!(" only check there is.");
}
Verified output of nothing_checks_a_shadow_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
──── As shipped: a log that looks fine and a leader that is not
counted [5, 3, 0] running total [5, 3, 0]
counted [4, 5, 1] running total [4, 5, 1]
counted [0, 2, 5] running total [0, 2, 5]
Leader: Ada
Nothing above looks alarming. Every round was counted, each
running total is a real number, and Ada is a real player.
Two things give it away, and only in hindsight:
totals = [0, 0, 0]
Every 'running total' was just that round echoed back, and
the accumulator never left zero. `leader` broke the all-zero
tie by index, so the report named whoever was first in the
player list. The honest leader is Ben, with 10.
──── What the compiler had to say about it: nothing
$ rustc --edition 2024 nothing_checks_a_shadow_kata.rs
$ <- no output, exit 0
`unused_variables` cannot fire: the shadow is read on the
next line, by the log. There is no type error: both are
[u32; 3]. And there is no shadowing lint in rustc to fire in
the first place. Three near-misses, all accidents of shape —
drop the log line and the first one would have caught it.
──── Which clippy lint finds it (recorded from a real run on this file)
(clippy needs a cargo crate, so this file is src/main.rs there)
$ cargo clippy -- -W clippy::shadow_same
$ <- finds NOTHING
$ cargo clippy -- -W clippy::shadow_unrelated
$ <- finds NOTHING
$ cargo clippy -- -W clippy::shadow_reuse
warning: `totals` is shadowed
--> src/main.rs:36:13 <- the bug
warning: `raw` is shadowed
--> src/main.rs:78:9 <- parse_limit, line 1
warning: `raw` is shadowed
--> src/main.rs:79:9 <- parse_limit, line 2
parse_limit(" 42 ") = 42
...which is correct code, flagged twice. It is the idiom
chapter 3.1 of the Book teaches.
──── The trade, stated plainly
shadow_same `let x = x;` junk always, the bug never
shadow_unrelated `let x = something_else;` silent here
shadow_reuse `let x = f(x);` catches it — and the idiom
The only lint that would have caught this bug is the one that
also condemns the good use of the feature. All three are
allow-by-default `restriction` lints, which is clippy saying
the same thing in its own vocabulary: a style commitment, not
a bug filter. Turn `shadow_reuse` on and you have banned
`let x = x.trim().parse()?` across the crate. That can be a
trade worth making — a large team, a codebase full of loops —
but make it deliberately, not hoping to catch one accumulator.
──── Three fixes, and the one to ship
1. `mut`, no shadow -> [9, 10, 6] leader Ben
2. fold, no accumulator -> [9, 10, 6] leader Ben
3. no name reused -> [9, 10, 6] leader Ben
Fix 2 is the one to ship, and the reason generalises past this
bug: it removes the BINDING, so the mistake has nowhere to
live. Fix 1 works, and is the honest counter-example to
'shadowing lets you keep everything immutable' — an
accumulator is supposed to survive the iteration, so a fresh
binding per pass is precisely the wrong tool. Fix 3 works and
relies on you not reusing a name, which is the discipline that
just failed.
The through-line: the compiler polices ownership, types and
exhaustiveness, and a shadow can be wrong in none of those
ways. When a shadow's type matches what it hides, you are the
only check there is.
The verified output¶
Verified output of nothing_checks_a_shadow.rs — regenerated by tools/run_examples.py, never hand-typed.
──── Step 1: The shadow that compiles clean
running total: 5
running total: 3
running total: 0
running total: 4
final total: 0
The running totals are 5, 3, 0, 4 — the scores themselves,
not a sum. Each iteration read the outer `total` (still 0),
added one score, and threw the result away at the brace.
Zero warnings. `rustc` compiled this without a word.
──── Step 2: Why nothing warned
There is no shadowing lint in rustc. `rustc -W help` mentions
'shadow' four times and every one is about trait items or glob
re-exports — none about a `let`.
Two things nearly catch it, and neither is a shadow check:
* `unused_variables`, if the shadow is never READ. Read it
once inside the loop — as the println above does — and
that lint has nothing to say.
* a TYPE MISMATCH downstream, if the shadow's type differs
from what later code expects: `E0308: mismatched types`.
Both are accidents. Same type, read once: total silence.
──── Step 3: Items get shadowed too, just as quietly
outer threshold() -> 50
inner threshold() -> 5 <- a different function
after the block -> 50
A `fn` inside a block shadows one outside it, with no
diagnostic at all. `fn` and `let` share the VALUE namespace,
so a variable can hide a function too — and that one rustc
does catch, because calling a u32 is a type error:
error[E0618]: expected function, found `u32`
| this function of the same name is available here,
| but it's shadowed by the local binding
Note rustc's own word for it there: shadowed.
──── Step 4: The same shape, where the compiler DOES stop you
job 1: Ada
job 2: Ada
job 3: Ada
original still here: Ada
Delete that `let name = name.clone();` and it does not
compile: E0382, 'value moved into closure here, in previous
iteration of loop'. Structurally the SAME shadow-in-a-loop as
step 1 — and this one is a hard error.
The difference is not the shadow. It is that ownership is
checked and arithmetic is not. Step 1 lost a number, which
no rule forbids; step 4 would have lost a String twice over,
which every rule forbids.
──── Step 5: Three ways to write step 1 so it cannot go quiet
1. `mut`, no shadow -> 12
2. no accumulator at all -> 12
3. a name that cannot collide-> 12
This is the one place `mut` genuinely beats shadowing, and
it is the opposite of the usual advice: an accumulator is
supposed to survive the iteration, so the thing shadowing
gives you — a fresh binding each time — is exactly the bug.
Option 2 is the one to ship: with no binding to shadow,
the mistake has nowhere to live.
Run it yourself:
rustc --edition 2024 18_Ownership/nothing_checks_a_shadow/examples/nothing_checks_a_shadow.rs -o /tmp/ncas && /tmp/ncas
Traps¶
- Believing the borrow checker is watching. It watches ownership. A shadow that loses a number violates nothing it knows about, and the identical shadow that would lose a
Stringis a hard error — same construct, opposite outcome, and the shadow is not what decided it. letinside a loop body, when you meant to accumulate. The single most common way this bites. An accumulator has to outlive the iteration, and a fresh binding per pass is the exact opposite of that.- Reading a clean build as a clean bill of health. Same type plus one use of the value equals silence. The absence of a warning here carries no information.
- Turning on
shadow_reuseto catch one bug. It also flagslet x = x.trim().parse()?, which is the idiom the Book teaches. Enable it as a style decision for a whole codebase, or not at all. - Assuming
shadow_unrelatedis the strict one. Its name suggests it catches the careless cases, but an accumulator reuses the shadowed value, so it stays silent on exactly the bug you want. - Shadowing a name that is also a function. In an inner block a
fnshadows an outerfnwith no diagnostic; the reverse is caught only asE0618, and only when you actually call it.
See also¶
- Shadowing and
unwrap— what shadowing is for, and the type change that makes it worth having - A shadow does not drop — what happens to the value the shadow hid: nothing, until the end of the scope
- When to shadow — the judgement call this page leaves open: what the feature buys that
mutcannot, the idioms worth copying, and the three bugs that compile - SHADOWING.md — the map of all four shadowing lessons, in reading order
- What a warning is asking —
unused_variables, the near-miss above, and what_nameactually answers - Ownership and moves — the rule that is enforced, and why it turns the same shape into
E0382 shadow_reuse↗,shadow_same↗,shadow_unrelated↗ — the three lints, and therestrictiongroup they sit in- The Rust Book, ch. 3.1 — Shadowing ↗
Po polsku¶
rustc nie ma ostrzeżenia dla przesłoniętej zmiennej. To zaskakuje, bo z doświadczenia wygląda, jakby coś tego pilnowało — ale tym „czymś” jest zwykły błąd typów, na który źle napisane przesłonięcie zwykle po drodze wpada. Kiedy typy się zgadzają, nic już nie stoi między tobą a złą odpowiedzią.
Trzy linty widzą przesłanianie, ale żaden nie jest domyślnie włączony, a ten użyteczny (clippy::shadow_unrelated) należy do grupy restriction — czyli takiej, która nie jest rekomendowana do włączania na całym projekcie, bo generuje mnóstwo szumu na idiomatycznym kodzie. To jest realny koszt, nie formalność: włączenie go oznacza sprzeciwianie się stylowi, którym napisana jest sama biblioteka standardowa.
Ciekawostka terminologiczna, przez którą po polsku łatwo się pogubić: przesłaniane są nie tylko zmienne, ale i elementy (items) — funkcje, typy, moduły. A część społeczności twierdzi, że let x = x + 1; w ogóle nie jest przesłanianiem w klasycznym sensie, bo nie ma tu dwóch zasięgów, tylko dwa kolejne wiązania w jednym. Spór jest o nazwę, nie o zachowanie.
Szukaj po polsku: przesłanianie zmiennych · lint clippy shadow · clippy::shadow_unrelated · rust shadowing lint