Shadowing ist eines der Sprachelemente, das C-, Java- oder Python-Programmierer beim ersten Anblick irritiert: warum darf eine let-Bindung mit dem gleichen Namen noch einmal geschrieben werden, ohne dass der Compiler protestiert? Die Antwort: es handelt sich gar nicht um eine Re-Assignment, sondern um eine neue Bindung, die die alte überlagert. Das öffnet Patterns, die mit mut allein nicht möglich wären — etwa einen Wert von String zu i32 zu wandeln und dabei den Namen zu behalten. Dieser Artikel definiert Shadowing präzise, grenzt es von mut ab und zeigt die idiomatischen Anwendungsfälle.
Was Shadowing ist
Shadowing entsteht, sobald ein zweites let mit dem gleichen Namen kommt:
fn main() {
let x = 5;
let x = x + 1; // neue Bindung, überlagert die alte
let x = x * 2; // wieder eine neue Bindung
println!("{x}"); // 12
}Aus Compiler-Sicht geschieht hier folgendes: nach der ersten Zeile existiert eine Bindung x = 5. In der zweiten Zeile entsteht eine neue, eigenständige Bindung x = 6, die innerhalb dieses Scopes den alten Namen verdeckt (englisch to shadow). Die alte Bindung existiert intern weiterhin — sie ist nur unter dem Namen x nicht mehr erreichbar.
Sobald der Scope endet, sind beide weg.
Drei Eigenschaften, die Shadowing definieren
- Es ist eine neue Bindung. Sie hat einen eigenen Speicher-Slot, ein eigenes Lifetime, ihren eigenen Typ.
- Sie verdeckt den alten Namen. Spätere Code-Zeilen sehen nur noch die neue Bindung.
- Der Typ darf wechseln. Anders als bei
mutist hier ein Typ-Wechsel ausdrücklich erlaubt — weil es ja eine neue Bindung ist.
Shadowing vs. mut — der zentrale Unterschied
mut und Shadowing fühlen sich oberflächlich ähnlich an, sind aber unterschiedliche Werkzeuge.
| Aspekt | mut | Shadowing |
|---|---|---|
| Neue Bindung? | nein, gleiche Bindung neu beschrieben | ja, eigene neue Bindung |
| Typ-Wechsel? | nein | ja |
| Speicher-Slot | unverändert | neuer Slot |
| Lifetime | gleich wie ursprüngliche Bindung | neu, beginnt ab dem zweiten let |
| Konzept | Mutation | Re-Definition |
Beispiel, das den Unterschied klar macht:
fn main() {
let eingabe = "42";
let eingabe: i32 = eingabe.parse().expect("Kein Integer");
println!("Doppelt: {}", eingabe * 2);
}Hier wechselt eingabe vom Typ &str zum Typ i32. Mit mut würde das nicht gehen — eine mut-Bindung erlaubt nur Werte des gleichen Typs.
fn main() {
let mut eingabe = "42";
// eingabe = eingabe.parse::<i32>().expect("…");
// Fehler E0308: expected `&str`, found `i32`
}Das ist die häufigste Praxis-Motivation für Shadowing: Format-Wechsel ohne neuen Namen erfinden zu müssen. Statt eingabe_str und eingabe_int zu jonglieren, hat ein Wert über den Verlauf der Funktion verschiedene Darstellungen — alle unter dem gleichen Namen.
Idiomatische Patterns
Shadowing ist in idiomatischem Rust überall — drei Patterns sind besonders verbreitet.
Pattern 1: Parse-and-Convert
use std::env;
fn main() {
let arg = env::args().nth(1).expect("Argument fehlt");
let arg: u32 = arg.parse().expect("Keine Zahl");
println!("Verarbeite: {arg}");
}Erst hast du den String, dann die geparste Zahl — beides unter dem gleichen Namen.
Pattern 2: Trim/Normalize
fn validiere(eingabe: String) -> bool {
let eingabe = eingabe.trim(); // &str
let eingabe = eingabe.to_lowercase(); // String
["ja", "yes", "j"].contains(&eingabe.as_str())
}Jede Zeile ist eine Transformation. Statt eingabe_trimmed, eingabe_lower zu erfinden, läuft alles unter eingabe. Der Code liest sich wie eine Datenfluss-Beschreibung.
Pattern 3: Block-Shadowing für temporäre Werte
fn main() {
let temperatur = 20.0;
let beschreibung = {
let temperatur = temperatur * 9.0 / 5.0 + 32.0; // Umrechnung
format!("{:.1} °F", temperatur)
};
println!("Außen: {} °C / {}", temperatur, beschreibung);
}Innerhalb des Blocks ist temperatur der Fahrenheit-Wert. Nach dem Block ist die ursprüngliche Celsius-Bindung wieder sichtbar. Klassisches RAII-artiges Pattern: ein Hilfswert nur dort, wo er gebraucht wird.
Shadowing über Block-Grenzen
Innerhalb eines Blocks erzeugt jede Shadowing-Bindung eine neue Bindung im selben Scope. Beim Verschachteln ändert sich das Bild:
fn main() {
let x = 5;
println!("außen vor Block: {x}");
{
let x = "ein String";
println!("im Block: {x}");
}
println!("außen nach Block: {x}");
}Ausgabe:
außen vor Block: 5
im Block: ein String
außen nach Block: 5Die innere Bindung lebt nur innerhalb des Blocks. Nach dem Block ist die äußere wieder sichtbar — die Shadowing-Wirkung war strikt lokal.
Funktions-Parameter shadowen
Auch Funktions-Parameter können geshadowed werden:
fn verarbeite(eingabe: &str) -> String {
let eingabe = eingabe.trim();
let eingabe = eingabe.to_uppercase();
eingabe
}Der Parameter eingabe ist die erste Bindung. Die zwei lets shadowen ihn nacheinander. Der originale Parameter ist nach dem ersten let nicht mehr erreichbar — wirkt sauber und konsequent.
Lifetime-Implikationen
Eine Shadowing-Bindung beginnt erst ab ihrer Definition. Die alte Bindung ist davor noch normal verwendbar.
fn main() {
let text = String::from("Hallo");
let laenge = text.len(); // text noch verfügbar
let text = text.to_uppercase(); // Shadowing — text ist jetzt der neue Wert
let _ = laenge;
println!("{text}");
}Wichtig: in let text = text.to_uppercase(); ist text auf der rechten Seite noch der alte Wert. Erst durch das = und das Semikolon entsteht die neue Bindung.
Drop der alten Bindung
Wenn die alte Bindung einen Drop-Type hält (z. B. ein String oder ein File), ist eine berechtigte Frage: wann wird er gedroppt?
Bei Shadowing wird die alte Bindung erst am Ende des umschließenden Scopes gedroppt, nicht sofort beim Shadowing. Sie ist nur unter dem alten Namen nicht mehr erreichbar — der Wert existiert noch.
struct Loud(String);
impl Drop for Loud {
fn drop(&mut self) {
println!("Drop: {}", self.0);
}
}
fn main() {
let l = Loud("erst".into());
let l = Loud("dann".into());
println!("Funktions-Ende erreicht");
}Ausgabe:
Funktions-Ende erreicht
Drop: dann
Drop: erstBeide Loud-Instanzen leben bis zum Funktions-Ende. Reihenfolge des Drops: LIFO (Last In, First Out) — dann zuerst, erst danach. Eine Bindung wird nicht magisch früher entsorgt, nur weil ihr Name geshadowed wurde.
Vergleich zu anderen Sprachen
Wer aus anderen Sprachen kommt:
- JavaScript/TypeScript mit
let/const: kennt Shadowing nicht innerhalb des gleichen Blocks —let x = 1; let x = 2;ist ein SyntaxError. Nur über Block-Grenzen hinweg geht es. - Python: kennt kein Shadowing-Konzept — jede Zuweisung ist eine Mutation des gleichen Slots, der Typ kann zwischendurch wechseln.
- Java/C#: kennt Shadowing nur bei Member-Variablen (Klassen-Felder durch Subklassen oder Methoden überdeckt). Lokale Variablen mit gleichem Namen sind verboten.
- C/C++: kennt es über Block-Grenzen hinweg, im gleichen Block aber nicht. C++ mit
auto x = ...; auto x = ...;im gleichen Block: Fehler. - Haskell/OCaml: kennen Shadowing als alltägliches Pattern — Rust hat das von dort.
Die Akzeptanz im gleichen Scope ist die rust-spezifische Eigenheit. Sie ist gewollt: ohne sie wäre der Parse-and-Convert-Pattern (siehe oben) deutlich umständlicher.
Besonderheiten
Shadowing erlaubt sogar den Wechsel zwischen mut und non-mut.
let mut x = vec![1,2,3]; x.push(4); let x = x; macht die Bindung danach immutable. Idiomatisches Pattern, um nach einer Aufbau-Phase einen Wert „einzufrieren" — z. B. einen Vec, der nach dem Befüllen nicht mehr verändert werden soll.
Shadowing kann den Typ vereinfachen.
let parsed = "42".parse::<i32>(); ergibt Result<i32, _>. let parsed = parsed.unwrap(); macht daraus i32. Statt zwei Bezeichner zu erfinden, läuft alles unter parsed — und beim Lesen folgt die Code-Linie der Daten-Linie.
Shadowing ist nicht das gleiche wie Re-Assignment.
let mut x = 5; x = 6; ist Mutation des gleichen Slots — die alte Zahl 5 wird überschrieben. let x = 5; let x = 6; legt einen neuen Slot an. Bei Copy-Typen wie i32 ist der Unterschied unsichtbar; bei Drop-Typen wie String ist er sichtbar (Drop-Reihenfolge, Move-Semantik).
Compiler optimiert Shadowing oft weg.
Im LLVM-Codegen werden viele Shadowing-Bindungen zu einem einzigen Register zusammengefasst, wenn der Typ gleich bleibt und keine Drops dazwischen sind. Die mentale Klarheit von Shadowing ist also „kostenlos" — Performance-Verlust gegenüber mut gibt es nicht.
Shadowing über Modul-Grenzen geht nicht.
Im Inneren einer Funktion oder eines Blocks ist Shadowing ein lokaler Mechanismus. Modul-Items (Funktionen, Konstanten, Typen) können nicht durch ein let mit gleichem Namen geshadowed werden — wohl aber durch ein use-Statement, das einen anderen Namen importiert.
Clippy warnt bei „verdächtigem“ Shadowing.
Der Lint clippy::shadow_unrelated (in clippy::restriction) protestiert, wenn ein Shadowing einen komplett anderen Wert für den gleichen Namen einführt — z. B. let x = compute_a(); let x = compute_b(); ohne Bezug zwischen den beiden. Wenn so etwas absichtlich ist, lieber andere Namen verwenden.
Block-Shadowing ist eine sehr saubere Form von Mini-Scope.
Ein { let x = ...; ... x } ist häufig sauberer als eine Hilfsfunktion zu erfinden. Drop am Blockende, kein Polluten des äußeren Scopes — eine kostenlose Form von „Inline-Funktion" ohne Funktion.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Shadowing
- Rust by Example – Variable Shadowing
- Rust Reference – Statements
- Clippy –
shadow_unrelatedLint