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.
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
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-Container — Vec<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
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
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.
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?
// 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-ArgsDer :: 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
fn neu<T>(x: T) -> Vec<T> { vec![x] }
fn main() {
let v = neu::<String>(String::from("a"));
let _ = v;
}Generische Methoden
fn main() {
let parsed = "42".parse::<u32>().unwrap();
assert_eq!(parsed, 42);
}Generische Typen
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
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.
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
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
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
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
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
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
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
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
- Rust Reference – Paths
- Rust by Example – Phantom Types
- Rust Issue – Turbofish-Diskussion
- The Rust Book – Generic Functions