Rust ist eine statisch typisierte Sprache mit überraschend wenig Typ-Annotationen im täglichen Code. Der Grund ist ein leistungsfähiger Type Inferenz-Algorithmus, der in den meisten Fällen den passenden Typ aus dem Kontext erschließt. Anders als bei Haskell oder ML, wo die Inferenz oft auch über Funktionsgrenzen hinweg läuft, ist sie in Rust lokal pro Funktion — was eine bewusste Designentscheidung ist: Funktions-Signaturen bleiben dadurch lesbare API-Verträge. Dieser Artikel erklärt, wie die Inferenz arbeitet, wo sie hilft, wo sie an Grenzen stößt und wie der Turbofish-Operator (::<T>) gezielt eingreift.

Was Type Inference in Rust tut

Inferenz heißt: der Compiler füllt Typ-Informationen aus, die du nicht explizit hingeschrieben hast.

Rust Inferenz in Aktion
fn main() {
    let a = 42;                    // Compiler folgert: i32
    let b = 3.14;                  // f64
    let c = "Hallo";               // &'static str
    let d = (1, 2.0, "drei");      // (i32, f64, &str)
    let e = vec![1, 2, 3];         // Vec<i32>
    let f = a + 1;                 // i32 (aus a's Typ)
}

Der Algorithmus arbeitet bidirektional: Informationen fließen sowohl von Werten zu Bindungen als auch von Verwendungen zurück zu Werten.

Rust Bidirektional
fn main() {
    let leerer_vec = Vec::new();          // Typ noch unklar
    let mut v: Vec<u8> = leerer_vec;       // Annotation klärt es
    v.push(255);
    // — v ist Vec<u8>, leerer_vec war also auch Vec<u8>
}

Der Compiler sieht in Zeile 2 die Annotation Vec<u8> und folgert rückwirkend: Vec::new() in Zeile 1 muss Vec<u8> gewesen sein.

Wo Inferenz greift, wo nicht

Faustregel: Inferenz funktioniert innerhalb einer Funktion, Funktions-Signaturen bleiben explizit.

StelleInferenz?
let-Bindungenja
Closure-Parameter und -Rückgabeja
Generische Argumente bei Aufrufenja
Element-Typ in Container (Vec, HashMap, …)ja
Funktions-Parameternein, explizit
Funktions-Rückgabetypnein, explizit
const und staticnein, explizit
Struct- und Enum-Feldernein, explizit

Das ist eine bewusste Entscheidung. Eine Funktions-Signatur ist Teil deines öffentlichen API; sie soll lesbar und stabil sein, unabhängig vom Funktions-Body. Hätte Rust globale Inferenz wie Haskell, würde eine harmlose Body-Änderung die abgeleitete Signatur ändern und Aufrufer überall brechen.

Default-Typen: {integer} und {float}

Wenn der Compiler nach allen Constraints noch immer einen mehrdeutigen numerischen Typ hat, fällt er auf Defaults zurück:

  • {integer}i32
  • {float}f64
Rust Defaults
fn main() {
    let x = 42;            // i32, weil keine andere Info da ist
    let y = 0.1;           // f64
    let z: u8 = 200;       // u8, weil Annotation überstimmt Default
    let q = 5u64;          // u64 durch Suffix
}

Wenn schon irgendwo im Code ein konkreter Typ erzwungen wird, übernimmt die Inferenz den. Erst wenn nichts den Typ einschränkt, schlagen die Defaults zu.

Suffixe auf Literalen

Du kannst den Typ direkt an Literalen festschreiben:

Rust Suffixe
let a = 42_u8;
let b = 1_000_000_i64;
let c = 3.14_f32;

Praktisch in Tests und bei Bit-Operationen, wo man auf bestimmte Bitbreite angewiesen ist.

Der Turbofish — gezielter Eingriff

Wenn eine generische Funktion mehrdeutig ist und keine Annotation auf der linken Seite hilft, kommt der Turbofish zum Einsatz: ::<T>.

Rust Turbofish-Beispiele
fn main() {
    // parse() ist generisch über den Ziel-Typ
    let n = "42".parse::<i32>().unwrap();
    let f = "3.14".parse::<f64>().unwrap();

    // collect() ist generisch über den Container
    let zahlen = (1..=5).collect::<Vec<i32>>();
    let als_set = (1..=5).collect::<std::collections::HashSet<i32>>();

    // Vec::new() ohne Element-Hinweis braucht Hilfe
    let mut v = Vec::<u8>::new();
    v.push(0);
}

Der Name „Turbofish" stammt vom Fisch-Symbol — ::<> sieht aus wie ein stilisierter Fisch von der Seite.

Alternative: Annotation auf der Bindung

Statt Turbofish kannst du auch links annotieren:

Rust Beide Varianten
// Mit Turbofish
let n = "42".parse::<i32>().unwrap();

// Mit Bindungs-Annotation
let n: i32 = "42".parse().unwrap();

Beide sind valide. Wann was?

  • Turbofish ist näher an der Aufruf-Stelle und kürzer in Method-Chains. Liest sich oft natürlicher.
  • Annotation ist sinnvoll, wenn die Bindung Teil eines Strukts oder Funktions-Rückgabewerts ist, die ohnehin schon einen Typ deklarieren.

Turbofish in Method-Chains

Besonders nützlich bei längeren Chains, in denen ein einzelner Adapter den Typ präzisieren muss:

Rust In einer Iterator-Kette
let summe: u32 = (1..=10).sum();
// oder
let summe = (1..=10).sum::<u32>();

Iterator::sum ist generisch über den akkumulierten Typ — irgendwo muss der Compiler eine Festlegung sehen.

Inferenz bei Closures

Bei Closures ist die Inferenz besonders aktiv — sowohl die Parameter- als auch die Rückgabe-Typen werden meist erkannt:

Rust Closure-Inferenz
fn main() {
    // Aus dem Verwendungskontext (Iterator über Vec<i32>) klar:
    let doppelt = (1..=5).map(|x| x * 2).collect::<Vec<i32>>();
    println!("{:?}", doppelt);
}

Der Compiler weiß: (1..=5) ist ein Iterator<Item=i32>, map nimmt eine Closure Fn(i32) -> ?, die Rückgabe der Closure ist i32 * 2 = i32. Komplette Inferenz, ohne Annotation.

Manchmal hilft eine Annotation am Closure-Parameter:

Rust Mit Annotation
let komparator = |a: &i32, b: &i32| a.cmp(b);

Sinnvoll, wenn die Closure später woandershin gereicht wird und dort der Typ aus dem Kontext noch nicht greifbar ist (z. B. wenn du sie in einer eigenen Variable speicherst, bevor sie aufgerufen wird).

Fehler erkennen und beheben

Der häufigste Inferenz-Fehler:

Rust type annotations needed
fn main() {
    let v = Vec::new();
    // — Fehler:
    // error[E0282]: type annotations needed
    //   --> src/main.rs:2:9
    //    |
    // 2  |     let v = Vec::new();
    //    |         ^ consider giving `v` an explicit type
}

Drei mögliche Fixes:

  1. Annotation auf der Bindung: let v: Vec<i32> = Vec::new();.
  2. Turbofish auf der Funktion: let v = Vec::<i32>::new();.
  3. Spätere Verwendung verrät den Typ:
Rust Späterer Hinweis reicht
fn main() {
    let mut v = Vec::new();
    v.push(42_i32);
    println!("{:?}", v);
}

Der push(42_i32) legt den Element-Typ fest; rückwirkend wird Vec::new() als Vec<i32> aufgelöst. Inferenz schaut bis zu mehrere Statements voraus, solange sie im gleichen Scope sind.

Mehrdeutigkeit zwischen mehreren Typen

Rust Mehrere Kandidaten
fn main() {
    let n = "1".parse().unwrap();
    // Fehler: type annotations needed for `Result<_, _>`
    println!("{n}");
}

parse ist generisch über alles, was FromStr implementiert. "1" kann zu u8, i32, f64, … geparst werden. Lösung: einer der zwei üblichen Wege:

Rust Disambiguieren
let n: i32 = "1".parse().unwrap();
let n = "1".parse::<i32>().unwrap();

Inferenz und Generics in Funktions-Aufrufen

Bei einem Aufruf einer generischen Funktion versucht der Compiler, die Type-Parameter aus den Argumenten zu schließen.

Rust Inferenz bei generischer Funktion
fn paar<T>(a: T, b: T) -> (T, T) {
    (a, b)
}

fn main() {
    let p = paar(1, 2);              // T = i32 (aus Argumenten)
    let q = paar("a", "b");          // T = &str
    // let r = paar(1, "a");         // Fehler: T kann nicht sowohl i32 als auch &str sein
    let r = paar::<i32>(1, 2);       // explizit
}

Wenn die Argumente eindeutig den Type-Parameter festlegen, brauchst du keinen Turbofish. Erst wenn die Inferenz mehrdeutig ist (z. B. weil ein Wert irgendwo verloren geht oder es mehrere Möglichkeiten gibt), wirst du explizit.

Inferenz für höher-rangige Type-Parameter

Bei generischen Funktionen mit Rückgabe-Type-Parametern, die nicht in den Argumenten erscheinen, muss der Type-Parameter immer aus dem Verwendungs-Kontext kommen:

Rust Type-Param nur in Return
fn standardwert<T: Default>() -> T {
    T::default()
}

fn main() {
    let n: i32 = standardwert();             // T = i32
    let s = standardwert::<String>();         // T = String
    // let x = standardwert();                // Fehler: type annotations needed
}

Was Inferenz nicht sieht

Drei häufige Stolperfallen:

Across function boundaries

Inferenz endet an Funktions-Grenzen.

Rust Nicht über Funktionen hinweg
fn nimm_vec(v: Vec<i32>) {}

fn main() {
    // Compiler erkennt aus nimm_vec(...) NICHT, dass v: Vec<i32> sein muss,
    // wenn er die Annotation entfernt — die Signatur ist explizit,
    // also gilt sie als Constraint.
    let v = Vec::new();
    nimm_vec(v);                          // ok — Inferenz nutzt die Signatur
}

Das funktioniert hier durchaus — nimm_vec(v) constraint v auf Vec<i32>, und Inferenz greift bidirektional. Die Grenze betrifft Signatur-Inferenz, nicht Argument-Inferenz.

Default-Trait-Implementations

Bei Aufrufen von Trait-Methoden, die ihre Receiver-Typen polymorph haben, hilft der Compiler manchmal nicht weiter:

Rust Mehrdeutiger Trait-Methodenaufruf
use std::str::FromStr;

let _ = i32::from_str("42").unwrap();        // ok, explizit i32
let _ = <i32 as FromStr>::from_str("42").unwrap();  // ausführliche Form

Die ausführliche Form (<T as Trait>::methode(...)) hilft, wenn mehrere Traits den gleichen Methodennamen haben oder die Inferenz nicht weiter kommt.

Interessantes

Rust-Inferenz ist nicht Hindley-Milner pur.

Klassisches Hindley-Milner (wie in Haskell) inferiert auch Funktions-Signaturen. Rust beschränkt sich bewusst auf lokale Inferenz pro Funktion, weil Signaturen Teil der API-Stabilität sind. Es ist eher eine bidirektionale lokale Typ-Constraint-Lösung, dem Hindley-Milner-Vorbild ähnlich aber bewusst eingeschränkt.

_ ist auch in Typ-Positionen erlaubt.

let v: Vec<_> = ...; sagt: „der Container ist ein Vec, den Element-Typ folgert der Compiler selbst". Praktisch, wenn der Container-Typ wichtig ist (z. B. zur Annotation in Funktions-Rückgaben), der Element-Typ aber klar ist.

Der Turbofish heißt so, weil ::<> aussieht wie ein Fisch.

Schräg gelesen: die zwei Doppelpunkte sind Augen, die spitzen Klammern bilden die Schwanzflosse. Diese Mnemonic hilft beim Erinnern — und macht aus einer leicht ungewöhnlichen Syntax eine memorierbare Konvention.

Inferenz für i32 und f64 als Defaults ist eine bewusste Entscheidung.

i32 ist auf den meisten Plattformen so groß wie ein Maschinenwort und schnell. f64 ist die übliche Präzision für „normales" Rechnen. Die Defaults bilden ab, was die meisten Programmierer in der Mehrheit der Fälle wollen. Wer kleinere oder größere Typen braucht, annotiert explizit.

rust-analyzer kann inferred Typen einblenden.

In VS Code und anderen Editoren mit rust-analyzer kannst du Inlay-Hints aktivieren — der inferred Typ erscheint dann als ausgegrauter Hint hinter der Bindung. Sehr lehrreich am Anfang: man sieht, was die Inferenz für dich entschieden hat.

Type Inference und Generics sind eng verzahnt.

Sobald du generische Funktionen schreibst oder benutzt, ist Inferenz das, was den Code lesbar hält. Ohne sie müsstest du vec.iter().map::<i32, _>(|x| x.clone()).filter::<_, _>(|x| x > &0).collect::<Vec<i32>>() schreiben — mit Inferenz reicht vec.iter().map(|x| x.clone()).filter(|x| x > &0).collect::<Vec<_>>() und in den meisten Fällen sogar weniger.

Inferenz greift nicht vorausschauend über mehrere Anweisungen hinaus — sie schaut zurück.

Der Algorithmus erlaubt zwar, dass ein späteres push(42_i32) die Element-Inferenz eines früheren Vec::new() festlegt. Aber spätere move-Operationen oder Aufrufe können den Type nicht ändern, sondern nur ihn (falls offen) bestimmen. Bei Mehrdeutigkeit über mehrere Statements gewinnt nicht das letzte Statement, sondern derjenige Constraint, der den Typ am genauesten festlegt.

Funktions-Rückgaben mit impl Trait haben „opaque“ Typen.

Wenn eine Funktion -> impl Iterator<Item=i32> zurückgibt, kennt der Aufrufer den konkreten Typ nicht (z. B. Map<Filter<Range<i32>, …>>). Inferenz behandelt ihn als opaken Typ — das funktioniert, lässt sich aber nicht in eine konkrete Typ-Annotation überführen. Mehr im Traits- und Iteratoren-Kapitel.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Variablen & Bindungen

Zur Übersicht