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:

Rust Shadowing-Grundform
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 mut ist 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.

AspektmutShadowing
Neue Bindung?nein, gleiche Bindung neu beschriebenja, eigene neue Bindung
Typ-Wechsel?neinja
Speicher-Slotunverändertneuer Slot
Lifetimegleich wie ursprüngliche Bindungneu, beginnt ab dem zweiten let
KonzeptMutationRe-Definition

Beispiel, das den Unterschied klar macht:

Rust Typ-Wechsel mit Shadowing
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.

Rust Mit mut: Compile-Fehler
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

Rust CLI-Argument parsen
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

Rust Eingabe normalisieren
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

Rust Temporärer Hilfswert in einem Block
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:

Rust Verschachtelte Scopes
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:

Rust
außen vor Block:  5
im Block:         ein String
außen nach Block: 5

Die 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:

Rust Parameter shadowen
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.

Rust Reihenfolge zählt
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.

Rust Drop nach Shadowing
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:

Rust
Funktions-Ende erreicht
Drop: dann
Drop: erst

Beide 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

/ Weiter

Zurück zu Variablen & Bindungen

Zur Übersicht