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:

Rust C-Stil
int x = 300;
char c = x;       // Implicit truncation — c == 44

In Rust wird der Compiler-Fehler:

Rust Rust verlangt explizit
let x: i32 = 300;
// let c: u8 = x;       // Fehler E0308: expected u8, found i32
let c: u8 = x as u8;     // ok, explizit

Der 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.

Rust as-Beispiele
// 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.0

Die Semantik im Detail:

  • Integer-Erweiterung (z. B. i8i32): sicher, sign-extension für signed.
  • Integer-Verkleinerung (z. B. i32u8): 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 u32 mit 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:

Rust From-Beispiele
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 exakt

Welche Konvertierungen sind verlustfrei? Eine Auswahl:

Von → NachVerlustfrei?
u8u16/u32/u64/u128/i16/i32/i64/i128/f32/f64/usize
i8i16/i32/i64/i128/f32/f64/isize
u16u32/u64/u128/i32/i64/i128/f32/f64
u32u64/u128/i64/i128/f64✓ (nicht zu f32 — Präzisionsverlust möglich)
u64u128/i128
i64i128/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 u32f32 will, muss as nutzen — bewusst.

Für die Korrektheit-orientierte Sicht:

  • u8 passt in alle signed Typen ≥ i16 und alle unsigned ≥ u8.
  • u16 passt in alle signed ≥ i32.
  • u32 passt in alle signed ≥ i64.
  • Float-Conversions sind nur exakt für genügend große Floats: u8/u16/i8/i16f32, u32/i32f64.

Into bei generischen Funktionen

Into ist besonders nützlich in generischen Funktions-Signaturen:

Rust Generic mit Into
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:

Rust TryFrom-Beispiele
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

Rust TryInto in Chain
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

SituationEmpfehlung
Du weißt sicher, dass es passtas oder From::from
Konvertierung ist garantiert verlustfreiFrom::from / into()
Konvertierung könnte fehlschlagenTryFrom::try_from
Hot-Path, jedes Nanosekunde zähltas
Float ↔ Intas (kein From dafür)
Bit-Manipulation, gewollte Truncationas
User-Input, unsichere QuellenTryFrom
Generic API, möglichst flexibelInto<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:

Rust isize/usize Conversions
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:

Rust Eigene Konvertierung
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:

Rust Port-Validierung
#[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:

Rust API-Count
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 u32try_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:

Rust serde_json-Konvertierung
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:

Rust Hash-Mix
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:

Rust Domain Conversion
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

/ Weiter

Zurück zu Primitive Datentypen

Zur Übersicht