Der Turbofish ::<T> ist eine ungewöhnlich aussehende Syntax, die du in Rust-Code immer wieder triffst. Sein offizieller Name ist path::<generic_args> — Konsumenten nennen ihn wegen des Fisch-artigen Aussehens „Turbofish". Funktional ist er die explizite Form, einen Typ-Parameter anzugeben, wenn die normale Type-Inference nicht ausreicht. Klassische Stellen: parse::<i32>("42"), collect::<Vec<_>>(). Dieser Artikel zeigt, wann der Turbofish nötig ist, wann er weggelassen werden kann, und warum er ausgerechnet diese seltsame Syntax bekommen hat.

Was der Turbofish macht

Generic-Funktionen und -Methoden haben Type-Parameter, die normalerweise vom Compiler aus dem Kontext inferiert werden. Der Turbofish ist die Möglichkeit, sie explizit anzugeben.

Rust Turbofish-Syntax
fn main() {
    // Mit Turbofish — Typ direkt am Methode angegeben
    let n = "42".parse::<i32>().unwrap();

    // Ohne Turbofish — Typ wird über Type-Annotation inferiert
    let m: i32 = "42".parse().unwrap();

    assert_eq!(n, m);
}

Beide Varianten sind semantisch identisch — der Typ-Parameter von parse wird auf i32 gesetzt. Bei der ersten Form ist es direkt sichtbar an der Aufruf-Stelle. Bei der zweiten Form ergibt es sich aus der Type-Annotation der Bindung.

Die Syntax ::<T> ist auf den ersten Blick verwirrend. Sie ergibt sich aus zwei Teilen: :: ist der Pfad-Trenner (wie in String::new()), <T> sind die Generic-Argumente. Zusammen: „auf diesem Pfad mit diesen Generic-Argumenten".

Wann Turbofish nötig ist

In den meisten Fällen kann der Compiler den Typ-Parameter selbst inferieren. Es gibt aber Situationen, wo das nicht geht.

collect() ohne Ziel-Typ

Rust collect-Problem
fn main() {
    // FEHLER — Compiler weiß nicht, in welchen Container collected werden soll
    // let v = (1..=5).collect();

    // OPTION 1: Type-Annotation
    let v: Vec<i32> = (1..=5).collect();

    // OPTION 2: Turbofish
    let v = (1..=5).collect::<Vec<i32>>();

    // OPTION 3: Turbofish mit _ für inferier-bare Teile
    let v = (1..=5).collect::<Vec<_>>();
}

collect() ist die meistbekannte Stelle, wo Turbofish nötig wird. Die Methode ist generisch über den Ziel-ContainerVec<T>, HashSet<T>, BTreeMap<K, V>, String und viele andere sind möglich. Ohne Kontext kann der Compiler nicht entscheiden.

Drei Wege, das Problem zu lösen — alle gültig, je nach Stil. Die Vec<_>-Variante ist besonders praktisch: das _ lässt den Compiler den Element-Typ inferieren, du sagst nur „ich will einen Vec, aber den Element-Typ kannst du herausfinden".

parse() ohne Ziel-Typ

Rust parse-Problem
fn main() {
    // FEHLER — welcher Typ soll geparst werden?
    // let n = "42".parse().unwrap();

    // OPTION 1: Type-Annotation
    let n: i32 = "42".parse().unwrap();

    // OPTION 2: Turbofish
    let n = "42".parse::<i32>().unwrap();
}

parse() ist generisch über den Ziel-Typ — i32, f64, IpAddr, eigene Typen mit FromStr-Impl. Der Compiler kennt den Quell-String (&str), aber nicht das Ziel. Wieder zwei Wege: Type-Annotation oder Turbofish.

In der Praxis ist Turbofish bei parse() oft die kompaktere Wahl, weil du den Typ direkt am Aufruf siehst. Wenn der Wert in einer Variable mit explizitem Typ landet, reicht die Type-Annotation.

Generische Funktion mit Phantom-Parameter

Rust Phantom-Param
fn default_wert<T: Default>() -> T {
    T::default()
}

fn main() {
    // FEHLER — Compiler weiß nicht, welcher Default
    // let v = default_wert();

    // OPTION 1: Type-Annotation
    let v: Vec<i32> = default_wert();
    assert_eq!(v, Vec::<i32>::new());

    // OPTION 2: Turbofish
    let v = default_wert::<Vec<i32>>();
    assert_eq!(v, Vec::<i32>::new());
}

Wenn der Type-Parameter nur im Rückgabe-Typ auftaucht (nicht in einem Argument), kann der Compiler ihn nicht aus Argumenten ableiten. Ohne Context muss er explizit angegeben werden — entweder über Type-Annotation der Bindung oder Turbofish am Aufruf.

Wann Turbofish nicht nötig ist

In vielen Fällen reicht die normale Type-Inference und Turbofish wäre nur Lärm.

Rust Inference reicht
fn identitaet<T>(x: T) -> T { x }

fn main() {
    // Compiler infert T = i32 aus dem Argument
    let n = identitaet(42);

    // Turbofish wäre redundant:
    // let n = identitaet::<i32>(42);

    // Compiler infert T = String aus dem Argument
    let s = identitaet(String::from("Hallo"));
}

Wenn der Type-Parameter aus einem Funktions-Argument abgeleitet werden kann, ist Turbofish überflüssig — der Compiler hat alle Information, die er braucht. Die explizite Form macht den Code nur länger, nicht klarer.

Die Faustregel: Turbofish nur, wenn der Compiler ihn fordert. Ein gut platzierter Type-Annotation auf der Bindung ist oft die elegantere Alternative.

Warum die seltsame Syntax?

Die ::<T>-Form sieht ungewöhnlich aus — String::<i32>::new() wirkt fremd. Warum nicht einfach String<i32>::new()?

Die Antwort ist historisch und syntaktisch. In Rust ist < auch der Less-Than-Operator. Bei einem Ausdruck wie foo<i32>(x) weiß der Parser nicht: ist das ein Funktions-Aufruf mit Generic-Argument, oder ist das foo kleiner als i32, gefolgt von >(x) als Klammer-Ausdruck?

Rust Mehrdeutigkeit
// Hypothetisch — wäre mehrdeutig
// foo<i32>(x)
// Könnte sein:
//   a) Aufruf von foo mit Generic-Arg i32 und Argument x
//   b) (foo < i32) > (x) — als Vergleich

// Mit Turbofish — eindeutig
// foo::<i32>(x)
// Das :: macht klar: das nächste <> sind Generic-Args

Der :: vor den spitzen Klammern eliminiert die Mehrdeutigkeit. Der Parser weiß: nach :: kommt definitiv eine Pfad-/Typ-Komponente, kein Vergleichs-Operator. Diese eine Syntax-Regel löst das gesamte Mehrdeutigkeits-Problem.

In Typ-Positions (etwa bei Variable-Annotations: let v: Vec<i32>) gibt es diese Mehrdeutigkeit nicht — dort sind < und > immer Generic-Argumente. Deshalb braucht die Typ-Annotation kein Turbofish: let v: Vec<i32> = ....

In Ausdrucks-Positions (am Aufruf-Ort) ist die Mehrdeutigkeit aber real. Daher die ::-Form.

Turbofish in verschiedenen Kontexten

Der Turbofish funktioniert nicht nur bei Funktionen, sondern überall, wo generische Typen oder Funktionen referenziert werden.

Generische Funktionen

Rust Funktion
fn neu<T>(x: T) -> Vec<T> { vec![x] }

fn main() {
    let v = neu::<String>(String::from("a"));
    let _ = v;
}

Generische Methoden

Rust Methode
fn main() {
    let parsed = "42".parse::<u32>().unwrap();
    assert_eq!(parsed, 42);
}

Generische Typen

Rust Typ-Konstruktor
fn main() {
    let v = Vec::<i32>::new();
    // Hier in Typ-Position: Vec<i32> ohne Turbofish ginge auch.
    // In Expression: Vec::<i32>::new() — Turbofish nötig
    assert!(v.is_empty());
}

Bei Vec::<i32>::new() ist Vec der Typ und new() die Methode. Da das in einer Expression-Position steht (nicht in einer Type-Annotation), muss der Turbofish verwendet werden.

Generische Constants und Statics

Rust Const-Generic
fn main() {
    // Beispiel mit Const-Generic (mehr im eigenen Artikel):
    let arr = std::array::from_fn::<i32, 5, _>(|i| i as i32);
    assert_eq!(arr, [0, 1, 2, 3, 4]);
}

from_fn::<i32, 5, _> zeigt Turbofish mit mehreren Parametern — Typ i32, Const 5, Closure-Typ mit _ zur Inference.

Underscore in Turbofish

Mit _ kannst du einzelne Parameter dem Compiler überlassen.

Rust Underscore
fn main() {
    // Nur den Container-Typ angeben, Element-Typ inferieren:
    let v: Vec<_> = (1..=5).collect();

    // Mit Turbofish:
    let v = (1..=5).collect::<Vec<_>>();

    assert_eq!(v, vec![1, 2, 3, 4, 5]);
}

Vec<_> sagt: „ich will einen Vec, aber den Element-Typ kannst du selbst herausfinden". Das ist eine Mischung aus expliziter Angabe und Inference — du spezifizierst genau das, was der Compiler nicht ableiten kann, und überlässt den Rest.

Das ist besonders nützlich, wenn die Element-Typen sehr komplex sind (etwa verschachtelte Iterator-Adapter-Typen), du sie aber selbst nicht ausschreiben willst.

Stil-Empfehlungen

In der Praxis gibt es Konventionen, wann Turbofish die bessere Wahl ist und wann Type-Annotation.

Turbofish bevorzugen, wenn:

  • Der Typ-Parameter sehr nahe am Aufruf relevant ist (etwa parse::<u32>).
  • Mehrere Aufrufe in einer Pipeline den gleichen Typ brauchen.
  • Du auf den Rückgabewert direkt eine Methode aufrufen willst (.collect::<Vec<i32>>().iter()).

Type-Annotation bevorzugen, wenn:

  • Der Typ semantische Bedeutung für die Bindung hat.
  • Mehrere Werte im Scope zusammen den gleichen Typ teilen.
  • Der Aufruf-Ausdruck schon lang ist und ein zusätzlicher Turbofish ihn weiter überfrachten würde.

Beide Stile sind in produktivem Rust-Code verbreitet. rustfmt zwingt dich zu keinem von beiden — wähle nach Lesbarkeit.

Praxis: Turbofish im echten Code

Parsen verschiedener Typen

Rust Multi-Parse
fn main() {
    let input = "42";
    let als_i32 = input.parse::<i32>().unwrap();
    let als_f64 = input.parse::<f64>().unwrap();
    let als_u16 = input.parse::<u16>().unwrap();

    assert_eq!(als_i32, 42);
    assert_eq!(als_f64, 42.0);
    assert_eq!(als_u16, 42);
}

Hier ist Turbofish klar besser als Type-Annotations: bei drei verschiedenen Typen wären drei separate Bindungen mit jeweils eigener Annotation nötig. So sieht man direkt am Aufruf, was gefragt ist.

Sammeln in verschiedene Container

Rust Multi-Collect
use std::collections::{HashSet, BTreeMap};

fn main() {
    let pairs = vec![(1, "a"), (2, "b"), (3, "c")];

    // In HashSet
    let keys: HashSet<i32> = pairs.iter().map(|(k, _)| *k).collect();

    // In BTreeMap
    let map: BTreeMap<i32, &str> = pairs.iter().copied().collect();

    // Mit Turbofish — gleiche Logik
    let keys2 = pairs.iter().map(|(k, _)| *k).collect::<HashSet<i32>>();

    let _ = (keys, map, keys2);
}

collect ist der Klassiker für Turbofish. Bei zwei verschiedenen Sammlungen aus derselben Quelle kann die Turbofish-Form sogar lesbarer sein als eine separate Type-Annotation pro Bindung.

Method-Chain mit Turbofish

Rust Chain
fn main() {
    let inputs = ["1", "abc", "3", "x", "5"];

    // Direkt in Chain weiterverwenden — Turbofish ist hier nötig,
    // weil die Bindung nicht direkt eine Type-Annotation tragen kann
    let valid_count = inputs.iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .count();

    assert_eq!(valid_count, 3);
}

In Method-Chains ist Turbofish oft die einzige sinnvolle Variante. inputs.iter().filter_map(...).count() produziert kein Zwischenresultat, das du annotieren könntest — der Typ-Parameter muss inline am parse-Aufruf stehen.

Const-Generic mit Turbofish

Rust Const-Param
fn neuer_array<const N: usize>() -> [i32; N] {
    [0; N]
}

fn main() {
    let a3 = neuer_array::<3>();
    let a5 = neuer_array::<5>();
    assert_eq!(a3, [0; 3]);
    assert_eq!(a5, [0; 5]);
}

Const-Generics (siehe eigener Artikel) brauchen Turbofish, wenn der Const-Parameter nicht aus Argumenten abgeleitet werden kann. neuer_array::<3>() macht den gewünschten Array-Length explizit.

Underscore-Pattern

Rust Mit Underscore
fn main() {
    let raw = ["10", "20", "30"];

    // Element-Typ aus Filter-Map ableiten, nur Container fixieren
    let zahlen: Vec<_> = raw.iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();

    assert_eq!(zahlen, vec![10, 20, 30]);
}

Vec<_> lässt den Element-Typ aus dem Iterator inferieren. Der Compiler weiß aus parse::<i32>, dass die Items i32 sind, also wird Vec<_> zu Vec<i32>.

From-Konvertierung mit Turbofish

Rust From mit Turbofish
fn main() {
    // String::from akzeptiert verschiedene Quell-Typen
    let s1 = String::from("hallo");
    let s2 = String::from('a');
    let s3 = <String as From<char>>::from('z');     // Sehr explizit

    let _ = (s1, s2, s3);
}

Die explizit-explizite Variante <Type as Trait>::method ist die ausführlichste Form. Sie kommt zum Einsatz, wenn ein Typ mehrere Traits mit der gleichen Methode implementiert und du eindeutig sagen musst, welche du meinst. Sehr selten in normalem Code, aber gut zu kennen.

Default mit Turbofish

Rust Default-Wert
use std::collections::HashMap;

fn main() {
    let leer_map = HashMap::<String, i32>::new();
    let leer_vec = Vec::<u8>::new();
    let leer_string = String::new();        // Hier kein Turbofish nötig
    let _ = (leer_map, leer_vec, leer_string);
}

Bei generischen Konstruktoren ohne Argumente ist Turbofish oft die kompakteste Form. HashMap::<String, i32>::new() ist klarer als ein Type-Annotation auf der Bindung, besonders bei verschachtelten Generics.

Besonderheiten

Turbofish ist ::, nicht .

Das :: vor den spitzen Klammern ist Pflicht in Expression-Position. Es eliminiert die Mehrdeutigkeit mit den Less-Than/Greater-Than-Operatoren.

In Typ-Position kein :: nötig.

let v: Vec<i32> = ... ist in Type-Annotation-Position. Hier sind < und > eindeutig Generic-Argumente, kein :: nötig.

Turbofish vs. Type-Annotation — Stilfrage.

let v: Vec<i32> = (1..=5).collect() oder let v = (1..=5).collect::<Vec<i32>>() — beide funktionieren. Wähle nach Lesbarkeit. Bei Method-Chains ist Turbofish oft unvermeidlich.

_ in Turbofish lässt einzelne Parameter inferieren.

Vec<_> heißt: „ich will einen Vec, aber den Element-Typ kannst du selbst rausfinden". Sehr nützlich bei komplexen Element-Typen, die du nicht ausschreiben willst.

Turbofish ist nicht obligatorisch.

Der Compiler verlangt ihn nur, wenn er den Typ-Parameter nicht aus dem Kontext ableiten kann. In den meisten Fällen reicht Type-Inference aus Argumenten oder Type-Annotation der Bindung.

parse::() ist Turbofish-Klassiker.

Bei parse ist der Ziel-Typ nicht aus dem Quell-String ableitbar. Turbofish macht's direkt am Aufruf sichtbar. Type-Annotation als Alternative ist gleichwertig.

collect::() ebenfalls häufig.

Welcher Container — Vec, HashMap, HashSet — ist aus Iterator allein nicht ableitbar. Turbofish oder Type-Annotation müssen ihn spezifizieren.

Der Name kommt vom Aussehen.

::<> sieht aus wie ein stilisierter Fisch. Die Rust-Community hat sich auf den Namen „Turbofish" geeinigt, weil's einprägsamer ist als „path with generic arguments". Inoffizielle aber etablierte Bezeichnung.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Generics

Zur Übersicht