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
| Makro | Tut |
|---|---|
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.
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
println!("{} und {}", "Tom", "Jerry"); // Reihenfolge
println!("{0} und {1}, dann wieder {0}", "Tom", "Jerry"); // Index-basiertNamed Arguments
println!("{name} ist {alter}", name = "Anna", alter = 30);Captured Identifiers (seit Edition 2021)
let name = "Anna";
let alter = 30;
println!("{name} ist {alter}"); // captured aus ScopeCaptured 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:
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 namedDisplay 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.
#[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:
{[Argument]:[Fill][Align][Sign][#][0][Width][.Precision][Type]}Jeder Teil ist optional.
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
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
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. PrefixWidth und Precision als Argumente
Wenn die Width oder Precision dynamisch ist:
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:
trait Display {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result;
}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
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
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
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
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
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
- std::fmt – Format-Syntax komplett
- std::fmt::Display
- std::fmt::Debug
- The Rust Book – Macros
- thiserror auf crates.io
- num-format auf crates.io