format! ist Rusts Antwort auf C's printf und Pythons f-strings. Es generiert zur Compile-Zeit aus einem Format-String und Argumenten einen String — und vor allem: es prüft die Format-Argumente zur Compile-Zeit. Wer format!("{} {}", x) mit zu wenig Argumenten schreibt, bekommt einen Compile-Fehler, nicht einen Runtime-Crash. Dieser Artikel zeigt die Syntax, alle Format-Spezifikatoren (Width, Precision, Padding, Number-Bases), Named-Arguments und die zentrale Unterscheidung zwischen Display und Debug.

Die Familie der Format-Makros

MakroTut
format!Gibt formatierten String zurück
println!Schreibt auf stdout, hängt Newline an
print!Schreibt auf stdout, ohne Newline
eprintln!Schreibt auf stderr, mit Newline
eprint!Schreibt auf stderr, ohne Newline
write!Schreibt in einen Write-Empfänger (File, String, ...)
writeln!Wie write! plus Newline
panic!Panic mit formatierter Message
dbg!Debug-Print mit Datei + Zeile + Wert (devel-only)

Alle teilen sich denselben Format-String-Mechanismus. Wer eines beherrscht, kennt alle.

Rust Familie in Aktion
fn main() {
    let name = "Anna";
    let alter = 30;

    let s: String = format!("{name} ist {alter}");
    println!("{s}");

    eprintln!("Warnung: {name} alt: {alter}");

    let mut buf = String::new();
    use std::fmt::Write;
    write!(buf, "{name}={alter}").unwrap();
}

Die drei Argument-Stile

Positional Arguments

Rust Positional
println!("{} und {}", "Tom", "Jerry");           // Reihenfolge
println!("{0} und {1}, dann wieder {0}", "Tom", "Jerry");  // Index-basiert

Named Arguments

Rust Named
println!("{name} ist {alter}", name = "Anna", alter = 30);

Captured Identifiers (seit Edition 2021)

Rust Captured
let name = "Anna";
let alter = 30;
println!("{name} ist {alter}");      // captured aus Scope

Captured Identifiers sind seit der 2021-Edition stable und der idiomatische Stil — kürzer, lesbarer, weniger Tippfehler.

Wichtig: Wenn das Format-Argument ein Ausdruck ist (Methodenaufruf, Berechnung), funktioniert Captured nicht. Dann brauchst du Named oder Positional:

Rust Capture geht nur mit Identifiern
let s = "Hallo";
println!("{s}");                    // ok — Identifier
// println!("{s.len()}");            // Fehler — keine Method-Calls in Captures
println!("{}", s.len());            // Lösung: positional
println!("{laenge}", laenge = s.len());  // oder named

Display vs. Debug

Zwei grundlegende Formate:

  • {} — Display: für End-User-Output. Muss manuell implementiert werden.
  • {:?} — Debug: für Entwickler-Output. Wird per #[derive(Debug)] automatisch generiert.
Rust Display vs. Debug
#[derive(Debug)]
struct Punkt { x: i32, y: i32 }

impl std::fmt::Display for Punkt {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Punkt { x: 3, y: 5 };
    println!("{p}");        // (3, 5)         — Display
    println!("{p:?}");      // Punkt { x: 3, y: 5 }  — Debug
    println!("{p:#?}");     // Punkt {        — Pretty-Debug
                             //     x: 3,
                             //     y: 5,
                             // }
}

Faustregel: Implementiere Display für alles, was Endnutzern angezeigt wird. Derive Debug für (fast) alles, was du debuggen können willst.

Warum nicht „einfach Display automatisch"?

Weil Display eine Design-Entscheidung ist: wie soll dein Typ aussehen, wenn er in einer Fehlermeldung oder einem Log-Eintrag erscheint? Debug ist mechanisch (Feld-für-Feld), Display ist redaktionell. Eine Punkt-Coordinate als (3, 5) ist klarer als Punkt { x: 3, y: 5 }.

Format-Spezifikatoren

Innerhalb der {}-Klammern gibt es eine Mini-Sprache. Die volle Form:

Rust
{[Argument]:[Fill][Align][Sign][#][0][Width][.Precision][Type]}

Jeder Teil ist optional.

Width und Padding

Rust Width und Padding
println!("{:10}|", "x");         // "x         |"  — links, padding rechts
println!("{:>10}|", "x");        // "         x|"  — rechts
println!("{:^10}|", "x");        // "    x     |"  — zentriert
println!("{:*<10}|", "x");       // "x*********|"  — fill mit *
println!("{:*>10}|", "x");       // "*********x|"
println!("{:*^10}|", "x");       // "****x*****|"

Zahlen-Formatierung

Rust Zahlen
println!("{:5}", 42);            // "   42"  — Width 5
println!("{:05}", 42);           // "00042"  — Zero-Padding
println!("{:+}", 42);            // "+42"    — explizites Vorzeichen
println!("{:+05}", 42);          // "+0042"
println!("{:5}", -42);           // "  -42"

println!("{:.3}", 3.141592);      // "3.142"  — Precision 3
println!("{:8.3}", 3.141592);    // "   3.142"
println!("{:08.3}", 3.141592);   // "0003.142"
println!("{:e}", 1_234_567.89);   // "1.23456789e6"

Number Bases

Rust Hex, Octal, Binary
println!("{:x}", 255);           // "ff"
println!("{:X}", 255);           // "FF"
println!("{:o}", 255);           // "377"
println!("{:b}", 255);           // "11111111"

// Mit # für 0x/0o/0b-Prefix
println!("{:#x}", 255);          // "0xff"
println!("{:#b}", 5);            // "0b101"
println!("{:#010b}", 5);         // "0b00000101" — width 10 inkl. Prefix

Width und Precision als Argumente

Wenn die Width oder Precision dynamisch ist:

Rust Dynamische Width
let w = 10;
let p = 3;
println!("{:1$}",       42, w);            // Width aus 2. Arg = 10
println!("{:.1$}",      3.141592, p);      // Precision aus 2. Arg = 3
println!("{:width$.prec$}", 3.141592, width=10, prec=3);

$ markiert „aus Argument". Etwas obskur, aber gelegentlich nützlich.

Eigene Display- und Debug-Implementierungen

Beide Traits leben in std::fmt. Ihre Signatur ist identisch:

Rust Trait-Definition
trait Display {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result;
}
Rust Eigene Implementierung
use std::fmt;

struct Preis { eur: u32, cent: u8 }

impl fmt::Display for Preis {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{},{:02} €", self.eur, self.cent)
    }
}

fn main() {
    let p = Preis { eur: 19, cent: 90 };
    println!("{p}");        // "19,90 €"
    assert_eq!(p.to_string(), "19,90 €");
}

Eleganter Nebeneffekt: wer Display implementiert, bekommt automatisch ToString — und damit .to_string() umsonst.

Praxis: Format! im echten Code

Logging mit strukturierten Werten

Rust Strukturiertes Log
struct Order { id: u64, betrag_cent: u32, status: &'static str }

impl std::fmt::Display for Order {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "Order #{} ({},{:02}€, {})",
               self.id,
               self.betrag_cent / 100,
               self.betrag_cent % 100,
               self.status)
    }
}

fn main() {
    let o = Order { id: 12345, betrag_cent: 4990, status: "PAID" };
    println!("{o}");   // "Order #12345 (49,90€, PAID)"
}

Tabellenartige Ausgabe

Rust Tabelle
fn main() {
    let zeilen = [
        ("Anna",   28, "Berlin"),
        ("Bert",  103, "Hamburg"),
        ("Clara",  41, "München"),
    ];

    println!("{:<10} {:>4} {:<10}", "Name", "Alter", "Stadt");
    println!("{:-<10} {:->4} {:-<10}", "", "", "");
    for (name, alter, stadt) in zeilen {
        println!("{name:<10} {alter:>4} {stadt:<10}");
    }
    // Name       Alter Stadt
    // ---------- ---- ----------
    // Anna         28 Berlin
    // Bert        103 Hamburg
    // Clara        41 München
}

{:<10} links-alignieren mit Width 10, {:>4} rechts mit Width 4 — einfache Tabellen ohne externe Crate.

Hex-Dump

Rust Byte-Dump
fn hex_dump(daten: &[u8]) {
    for chunk in daten.chunks(16) {
        for byte in chunk {
            print!("{byte:02x} ");
        }
        // Padding wenn Chunk < 16
        for _ in chunk.len()..16 {
            print!("   ");
        }
        print!(" |");
        for &byte in chunk {
            let c = if (32..127).contains(&byte) { byte as char } else { '.' };
            print!("{c}");
        }
        println!("|");
    }
}

fn main() {
    hex_dump(b"Hallo, Welt! Mit ein bisschen mehr Inhalt.");
}

{:02x} für 2-stelliges Hex mit Zero-Padding. Klassisches Tool-Pattern für Binär-Datei-Inspektion.

URL-Builder mit Query-Encoding

Rust URL-Builder
fn baue_query(params: &[(&str, &str)]) -> String {
    let pairs: Vec<String> = params.iter()
        .map(|(k, v)| format!("{k}={v}"))
        .collect();
    pairs.join("&")
}

fn main() {
    let url = format!(
        "https://api.example.com/users?{}",
        baue_query(&[("page", "1"), ("limit", "20"), ("sort", "name")])
    );
    assert_eq!(url, "https://api.example.com/users?page=1&limit=20&sort=name");
}

(In Produktion natürlich URL-Encoding einbauen — percent-encoding-Crate.)

Error-Display mit Kontext

Rust Error-Typ
use std::fmt;

#[derive(Debug)]
enum ParseFehler {
    Leer,
    NichtNumerisch { input: String },
    Zubig { wert: i64, max: i64 },
}

impl fmt::Display for ParseFehler {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ParseFehler::Leer => write!(f, "Eingabe leer"),
            ParseFehler::NichtNumerisch { input } =>
                write!(f, "'{input}' ist keine Zahl"),
            ParseFehler::Zubig { wert, max } =>
                write!(f, "Wert {wert} überschreitet Maximum {max}"),
        }
    }
}

impl std::error::Error for ParseFehler {}

fn main() {
    let e = ParseFehler::Zubig { wert: 9999, max: 100 };
    println!("Fehler: {e}");
    // "Fehler: Wert 9999 überschreitet Maximum 100"
}

Eigene Error-Typen mit klarem Display sind Standard in jedem ernsthaften Rust-Projekt. Mit dem thiserror-Crate lässt sich die Display-Implementierung sogar per Macro generieren.

FAQ

Warum prüft format! die Argumente zur Compile-Zeit?

Weil es ein Makro ist, nicht eine Funktion. Der Compiler analysiert den Format-String zur Build-Zeit und vergleicht ihn mit der Argument-Liste. format!("{} {}", x) mit nur einem Argument — Compile-Fehler. Anders als C's printf, das zur Laufzeit explodieren würde.

Was ist der Unterschied zwischen format! und to_string()?

42.to_string() ist eine Methode auf jedem Display-Typ. format!("{}", 42) ist ein Makro mit Format-String. Bei einfachen Konvertierungen identisch, aber das Makro erlaubt Formatierung (format!("{:08x}", 42)), die Methode nicht.

Wann Display, wann Debug?

Display für End-User-Output (Logs, Fehlermeldungen, UI). Debug für Entwickler-Output (dbg!, Test-Failures, generische Diagnostik). Faustregel: jeder Typ braucht Debug (derive), Public-API-Typen sollten zusätzlich Display haben.

Kann ich Floats mit Tausender-Trennzeichen formatieren?

Nicht direkt aus der Stdlib. format!("{:.2}", 1234567.89) gibt 1234567.89. Für Tausender-Trennzeichen brauchst du das num-format-Crate (123_456.toFormattedString(&Locale::de)"123.456"). Locale-bewusste Formatierung ist nicht Stdlib-Pflicht.

println! ist nicht performance-frei.

Jeder Aufruf locked stdout, was bei Concurrency-Code teuer sein kann. Für viele Ausgaben kurz hintereinander besser: let stdout = io::stdout(); let mut lock = stdout.lock(); writeln!(lock, ...). In Hot-Paths macht das den Unterschied von 10×.

dbg! ist nur für Development.

dbg!(x) gibt [src/main.rs:5] x = 42 auf stderr aus und gibt den Wert weiter. Perfekt für ad-hoc-Debugging — aber in keinem produktiven Code lassen. Clippy hat clippy::dbg_macro als Warnung.

{:?} auf Vec verlangt T: Debug.

Wenn du einen Vec<MyStruct> debug-printen willst, muss MyStruct selbst Debug implementieren. #[derive(Debug)] auf dem Struct reicht — der Vec-Debug-Impl ist generisch.

Captured Identifiers vereinfachen, aber begrenzen.

println!("{name}") ist toll für simple Variablen. Bei Ausdrücken (x.len(), arr[0], 1 + 2) gibt's einen Fehler — dann positional. Manche Codebases bevorzugen aus Konsistenz immer positional, andere mischen je nach Lesbarkeit.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Strings & Text

Zur Übersicht