Rust hat keine impliziten numerischen Konvertierungen. Wer aus einem i32 einen u8 machen will, muss das explizit ausdrücken — und Rust gibt ihm dafür drei verschiedene Werkzeuge mit unterschiedlichen Garantien: den as-Cast (schnell, aber lossy), die Konvertierungs-Traits From und Into (garantiert verlustfrei) und TryFrom / TryInto (fallible, mit Result). Dieser Artikel zeigt jede Variante mit Beispielen, listet die typischen Fallen auf und gibt eine klare Empfehlung, wann welche zu wählen ist.
Warum keine impliziten Conversions?
In C oder JavaScript geht so etwas durch:
int x = 300;
char c = x; // Implicit truncation — c == 44In Rust wird der Compiler-Fehler:
let x: i32 = 300;
// let c: u8 = x; // Fehler E0308: expected u8, found i32
let c: u8 = x as u8; // ok, explizitDer Grund: implizite Konvertierungen verstecken potenzielle Bugs. Rust verlangt eine bewusste Entscheidung — welche der drei Konvertierungs-Methoden willst du nutzen?
Variante 1: as
as ist der direkteste, schnellste, gefährlichste Cast. Es macht, was die CPU mit den Bits macht — keine Prüfung, keine Warnung.
// Erweiterung (sicher, verlustfrei)
let a: i32 = 100;
let b: i64 = a as i64; // 100
// Einschränkung mit Truncation
let big: i32 = 300;
let small: u8 = big as u8; // 44 — die unteren 8 Bit
let neg: i32 = -1;
let unsig: u32 = neg as u32; // 4_294_967_295 — Zweierkomplement bleibt
// Float zu Integer
let f = 3.7_f64;
let i = f as i32; // 3 — schneidet ab (Richtung 0)
let big_f = 1e20_f64;
let i_max = big_f as i32; // i32::MAX (saturiert seit 1.45)
let nan_f = f64::NAN;
let i_nan = nan_f as i32; // 0
// Integer zu Float
let i = 1_000_000_i32;
let f = i as f64; // 1000000.0Die Semantik im Detail:
- Integer-Erweiterung (z. B.
i8→i32): sicher, sign-extension für signed. - Integer-Verkleinerung (z. B.
i32→u8): nimmt die unteren N Bits, ignoriert den Rest. - Signed ↔ Unsigned gleicher Bitbreite: bleibt das Bit-Pattern, ändert Interpretation.
- Integer ↔ Float: lossy bei großen Integern, saturiert bei Float→Int.
as ist niemals undefined behavior in heutigem Rust — alle Edge-Cases (NaN, Infinity, Out-of-Range) sind seit Rust 1.45 deterministisch.
Wann as nutzen?
- Du weißt sicher, dass die Werte in den Zielbereich passen (z. B.
index as u32mit garantiert positivem index). - Du willst bewusst truncieren (z. B. bei Bit-Manipulationen).
- Im Code-Hot-Path, wo der Performance-Vorteil zählt.
Sonst lieber From/Into oder TryFrom.
Variante 2: From und Into (verlustfrei)
Die Konvertierungs-Traits From<T> und Into<U> sind in der Standard-Library für alle garantiert verlustfreien Integer-Konvertierungen implementiert:
let a: u8 = 100;
let b: u32 = u32::from(a); // u8 passt immer in u32
let c: u64 = a.into(); // Mit Into
let d: i32 = -50;
let e: i64 = i64::from(d);
let f: u8 = 200;
let g: f64 = f.into(); // u8 → f64 ist immer exaktWelche Konvertierungen sind verlustfrei? Eine Auswahl:
| Von → Nach | Verlustfrei? |
|---|---|
u8 → u16/u32/u64/u128/i16/i32/i64/i128/f32/f64/usize | ✓ |
i8 → i16/i32/i64/i128/f32/f64/isize | ✓ |
u16 → u32/u64/u128/i32/i64/i128/f32/f64 | ✓ |
u32 → u64/u128/i64/i128/f64 | ✓ (nicht zu f32 — Präzisionsverlust möglich) |
u64 → u128/i128 | ✓ |
i64 → i128/f64 | ✓ (f64 mit Präzisionsverlust ab ~2⁵³) — eigentlich nicht! |
Tatsächlich: From<u32> for f32 und From<u64> for f64 existieren nicht in der Stdlib, weil Floats nur begrenzte Mantissen-Präzision haben. Wer u32 → f32 will, muss as nutzen — bewusst.
Für die Korrektheit-orientierte Sicht:
u8passt in alle signed Typen ≥i16und alle unsigned ≥u8.u16passt in alle signed ≥i32.u32passt in alle signed ≥i64.- Float-Conversions sind nur exakt für genügend große Floats:
u8/u16/i8/i16→f32,u32/i32→f64.
Into bei generischen Funktionen
Into ist besonders nützlich in generischen Funktions-Signaturen:
fn drucke<T: Into<i64>>(x: T) {
let als_i64 = x.into();
println!("{als_i64}");
}
fn main() {
drucke(5_i32);
drucke(10_u16);
drucke(-7_i8);
}Eine Funktion, die alles akzeptiert, was sich verlustfrei in i64 konvertieren lässt. Sehr idiomatisches Pattern in APIs.
Variante 3: TryFrom und TryInto (fallible)
Wenn eine Konvertierung fehlschlagen kann, sind TryFrom<T> und TryInto<U> die richtige Wahl. Beide geben Result zurück:
fn main() {
let big: i32 = 300;
let small = u8::try_from(big); // Err(TryFromIntError)
let ok = u8::try_from(50); // Ok(50)
match u8::try_from(300_i32) {
Ok(v) => println!("{v}"),
Err(e) => println!("Fehler: {e}"),
}
// Mit ?
fn verarbeite(x: i32) -> Result<u8, std::num::TryFromIntError> {
let y = u8::try_from(x)?;
Ok(y * 2)
}
}TryFrom-Konvertierungen sind für alle Integer-Kombinationen verfügbar (auch die verlustfreien — dort liefert TryFrom immer Ok). Das macht sie zu einer sicheren Default-Wahl, wenn du nicht sicher bist, ob ein Wert passt.
TryInto in Method-Chains
use std::convert::TryInto;
fn berechne(parts: &[i32]) -> Result<u8, &'static str> {
let sum: i32 = parts.iter().sum();
let als_u8: u8 = sum.try_into().map_err(|_| "Summe zu groß")?;
Ok(als_u8)
}try_into() ist sehr lesbar und integriert sich elegant mit dem ?-Operator.
Entscheidungs-Matrix
| Situation | Empfehlung |
|---|---|
| Du weißt sicher, dass es passt | as oder From::from |
| Konvertierung ist garantiert verlustfrei | From::from / into() |
| Konvertierung könnte fehlschlagen | TryFrom::try_from |
| Hot-Path, jedes Nanosekunde zählt | as |
| Float ↔ Int | as (kein From dafür) |
| Bit-Manipulation, gewollte Truncation | as |
| User-Input, unsichere Quellen | TryFrom |
| Generic API, möglichst flexibel | Into<T> Bound |
Faustregel: From/Into bevorzugen, wenn möglich; TryFrom bei unsicheren Werten; as nur, wenn die anderen nicht ausreichen oder Performance entscheidend ist.
isize und usize — Sonderfall
isize und usize haben plattformabhängige Größe (32 oder 64 Bit). Das macht ihre Konvertierungen tricky:
let i: usize = 100;
let j: u32 = i as u32; // Auf 32-bit verlustfrei, auf 64-bit ggf. Truncation
let k: u64 = i as u64; // Auf 64-bit verlustfrei, auf 32-bit Erweiterung
// From-Trait existiert NUR für „verlustfreie auf jeder Plattform"
// u32::from(usize) — nicht implementiert
let safe: u64 = u64::try_from(i).unwrap();
// Häufig: usize → u32 mit TryFrom
let n: usize = vec.len();
let als_u32: u32 = n.try_into()?;Standard-Empfehlung: für portable Code immer TryFrom / TryInto zwischen usize und festen Bitbreiten.
Custom Conversions
From, Into, TryFrom, TryInto sind Traits — du kannst sie für eigene Typen implementieren:
struct Meter(f64);
struct Kilometer(f64);
impl From<Meter> for Kilometer {
fn from(m: Meter) -> Self {
Kilometer(m.0 / 1000.0)
}
}
fn main() {
let m = Meter(1500.0);
let km: Kilometer = m.into();
println!("{}", km.0); // 1.5
}Wenn From<A> for B implementiert ist, bekommst du Into<B> for A kostenlos — die Stdlib hat eine generische Blanket-Implementierung dafür.
Praxis: Konvertierungen im echten Code
User-Input parsen und validieren
CLI-Tool akzeptiert eine Port-Nummer als String — sie muss in u16 passen und zusätzlich semantisch gültig sein:
#[derive(Debug)]
enum PortFehler {
KeinInteger,
AusserhalbBereich,
Privilegiert,
}
fn parse_port(input: &str) -> Result<u16, PortFehler> {
let als_zahl: i64 = input.parse().map_err(|_| PortFehler::KeinInteger)?;
let als_u16: u16 = u16::try_from(als_zahl).map_err(|_| PortFehler::AusserhalbBereich)?;
if als_u16 < 1024 {
return Err(PortFehler::Privilegiert);
}
Ok(als_u16)
}
fn main() {
for eingabe in ["8080", "65536", "abc", "443"] {
println!("{eingabe} -> {:?}", parse_port(eingabe));
}
}parse gibt einen Wert in einem großen Integer-Typ zurück; try_from verengt sicher zu u16. Beides zusammen mit ? ergibt eine sauber komponierte Validierungs-Pipeline.
Vec-Length zu API-Limit prüfen
Viele Web-APIs erwarten Counts als u32 — die Vec-Länge ist aber usize:
fn baue_request(items: &[String]) -> Result<Request, &'static str> {
let count: u32 = u32::try_from(items.len())
.map_err(|_| "zu viele Items für API")?;
Ok(Request { count, items: items.to_vec() })
}
# struct Request { count: u32, items: Vec<String> }Auf 64-bit-Plattformen passt usize nicht garantiert in u32 — try_from fängt den Edge-Case. Ein nakter as u32-Cast würde stillschweigend Truncation produzieren, was bei u32::MAX + 1 Items in einen falschen Count laufen würde.
JSON-Zahlen zu Datenbank-Typen
JSON liefert Zahlen oft als f64 (default in serde_json), die Datenbank erwartet aber i64:
fn json_zu_db_id(wert: f64) -> Result<i64, &'static str> {
if !wert.is_finite() { return Err("NaN oder Inf"); }
if wert.fract() != 0.0 { return Err("kein Integer"); }
if wert < i64::MIN as f64 || wert > i64::MAX as f64 {
return Err("außerhalb i64-Bereich");
}
Ok(wert as i64) // jetzt sicher
}Hier as bewusst und nach Validierung — drei Checks vorher schließen jede Falle aus (NaN, Nachkommateil, Out-of-Range). Erst dann darf der lossy Cast greifen.
Bit-Reinterpret zwischen Signed und Unsigned
Für Hash-Funktionen und Bitmanipulation ist die as-Reinterpretation häufig genau das, was man will:
fn mix(a: u64, b: u64) -> u64 {
let z = a.wrapping_add(b);
// FxHash-artiges Mixing
let rotated = z.rotate_left(13);
rotated.wrapping_mul(0xC2B2AE3D27D4EB4F)
}wrapping_*-Operationen statt as direkt — semantisch identisch (modulare Arithmetik), aber lesbarer und explizit erlaubt im Debug-Build (kein Overflow-Panic).
Custom-Konvertierung mit From-Impl
Domänen-spezifische Typen mit eigenem From:
struct Sekunden(u64);
struct Millisekunden(u64);
impl From<Sekunden> for Millisekunden {
fn from(s: Sekunden) -> Millisekunden {
Millisekunden(s.0 * 1000)
}
}
fn timeout_ms(dauer: impl Into<Millisekunden>) -> u64 {
dauer.into().0
}
fn main() {
let ms = timeout_ms(Sekunden(5));
assert_eq!(ms, 5000);
}impl Into<Millisekunden> als Parameter — der Aufrufer übergibt einfach Sekunden(5), die Konvertierung passiert automatisch. Eines der elegantesten Patterns für „akzeptiere mehrere kompatible Eingabe-Typen".
Häufige Stolperfallen
as bei Float zu signed Integer hat eine Subtilität.
Werte zwischen den Float-Limits werden truncated Richtung 0. NaN → 0. Werte über i32::MAX saturieren zu i32::MAX. Aber: Wenn ein Float exakt an einer Integer-Grenze liegt (z. B. 2147483648.0), wird er saturiert, nicht gerundet. Wer sauber rundet, sollte .round() oder .floor() vor dem Cast nutzen.
From for f32 existiert NICHT.
Weil f32 nur 24 Bit Mantisse hat, kann ein u32 (32 Bit Wertebereich) nicht garantiert exakt dargestellt werden. Wer trotzdem konvertieren will: as f32 (lossy) oder f32::from_bits(u32_bits) (Bit-Reinterpret — etwas anderes). Stdlib lässt dich nicht stillschweigend Präzision verlieren.
u8 as i8 ändert die Interpretation, nicht die Bits.
200_u8 as i8 ist -56 — das Bit-Pattern 0b11001000 bleibt, aber jetzt als signed gelesen. Das ist die Standard-Zweierkomplement-Interpretation. Sehr wichtig für Bit-Manipulationen, aber tückisch, wenn du semantisch konvertieren willst.
usize und u32 sind nicht beliebig austauschbar.
Auf 64-bit-Systemen ist usize 64 bit. Wer in einer Funktion vec.len() as u32 schreibt, bekommt auf großen Vecs Truncation. TryFrom oder try_into ist sicherer — und der Compiler-Hint, falls die Vec länger ist als u32::MAX.
From::from und Into::into sind identisch — wähle nach Lesbarkeit.
u64::from(5_u32) ist explizit und gut, wenn der Ziel-Typ wichtig ist. 5_u32.into() ist kompakter und gut, wenn der Ziel-Typ aus dem Context kommt (Annotation oder Funktions-Signatur). Funktional dasselbe.
Bei TryFrom ist der Error-Typ oft TryFromIntError.
std::num::TryFromIntError ist ein Zero-Sized-Error-Typ („out of range"). Wenn du in einer eigenen Fehler-Hierarchie arbeitest, konvertiere ihn mit ? und einem From-Impl, oder mit .map_err(...). Detail-Informationen über den ursprünglichen Wert sind in TryFromIntError NICHT enthalten.
From-Implementierungen sind „reflexive“ — T -> T ist eingebaut.
i32::from(5_i32) funktioniert (Identität). Das gibt es für jeden Typ über eine Stdlib-Blanket-Implementierung. Praktisch in generischen Funktionen, die T: Into<U> verlangen — wenn T == U, geht es trotzdem (Identitäts-Konvertierung).
TryFrom ist nicht nur für Integer.
Auch Strings (String::try_from(...)), eigene Typen mit Validierung, JSON-Werte und beliebige andere Domänen nutzen TryFrom. Das Pattern „validiere und wandle um" ist über den Trait standardisiert. Eigene Validierungs-Typen sollten TryFrom implementieren, nicht eigene parse_*-Methoden erfinden.
Weiterführende Ressourcen
Externe Quellen
- Rust Reference – Type cast expressions
- std::convert::From
- std::convert::Into
- std::convert::TryFrom
- std::num::TryFromIntError
- Rust API Guidelines – Conversions