Ferris Talk #6: Ein neuer Trick für die Formatstrings in Rust

Rust 1.58 hat bei den Captured Identifiers etwas Neues gelernt – die Funktionserweiterung bietet Anlass, einen Blick auf die Formatstrings zu werfen.

In Pocket speichern vorlesen Druckansicht 5 Kommentare lesen
Ferris Talk – Neuigkeiten zu Rust. Eine Heise-Kolumne von Rainer Stropek und Stefan Baumgartner für Rustaceans
Lesezeit: 9 Min.
Von
  • Rainer Stropek
Inhaltsverzeichnis

Das einfachste Programm, das man zum Einstieg in eine neue Programmiersprache schreibt, ist das berühmte "Hello World". In Rust sieht es so aus:

fn main() {
    println!("Hello World!");
}

Listing 1: "Hello World"

Das println-Kommando sticht sofort ins Auge. Tatsächlich handelt es sich um ein Makro, um genau zu sein, um ein Function-like Procedural Macro. Für Einsteiger und Einsteigerinnen ist die Tatsache, dass Rust schon für ein so einfaches Programm auf ein Makro zurückgreift, die erste Überraschung, die die Sprache bereithält. Wer bereits mehr mit Rust gemacht hat, weiß, dass das keine Ausnahme, sondern die Regel ist. Rust-Programme stecken in der Praxis voller Makros, die einem das Leben spürbar erleichtern. Wer Makros aus anderen Sprachen kennt und dort nicht liebgewonnen hat, den kann ich beruhigen: Makros in Rust sind ein mächtiges und stabiles Werkzeug.

Ferris Talks – die Kolumne für Rustaceans

Natürlich kann println mehr als nur Textkonstanten auf stdout ausgeben. Die wirkliche Power ergibt sich aus dem Formatstring, den das Makro entgegennimmt. Mit Rust 1.58 sind Formatstrings durch die neue Captured Identifiers-Funktion noch einmal kompakter und leichter lesbar geworden. Wir nehmen die aktuelle Spracherweiterung zum Anlass, um in dieser Ausgabe der Ferris Talks die Formatstrings in Rust genauer unter die Lupe zu nehmen und auch die erwähnten Captured Identifiers vorzustellen.

Rust bietet vier Basis-Makros zum Ausgeben auf stdout und stderr:

fn main() {
    print!("Hello ");               // Print without \n
    println!("World!");             // Print with \n

    eprint!("Oops, something ");    // Print to stderr without \n
    eprintln!("bad happened!");     // Print to stderr with \n
}

Listing 2: Ausgabe auf stdout und stderr

Will man Variablenwerte in die Ausgabe einbauen, baut man "Lücken" in den Formatstring ein. Das folgende Beispiel zeigt das Grundprinzip. Anschließend geht es um sämtliche Varianten, wie Formatstrings aufgebaut sein können.

fn main() {
    // Print variable value
    let answer = 42;
    println!("The answer is {}", answer);

    // Use format string to build string for further processing
    // (here: convert answer string to base64 string).
    let answer_base64 = base64::encode(format!("The answer is {}", answer));
    println!("{}", answer_base64);
}

Listing 3: Platzhalter für Variablen im Formatstring

Das oben gezeigte Beispiel enthält neben println ein weiteres, häufig verwendetes Makro: format. Es ermöglicht das Verwenden von Formatstrings zum Zusammenbauen einer Ergebniszeichenkette im Hauptspeicher. Listing 3 baut mit format einen Text zusammen, um ihn anschließend in Base64 zu encodieren.

Aufmerksame Leserinnen und Leser wundern sich, warum in der letzten Zeile von Listing 3 ein Formatstring zum Einsatz kam. Ist println!("{}", answer_base64); notwendig oder ließe sich nicht einfach println!(answer_base64); schreiben? Tatsächlich ist der Formatstring in diesem Fall verpflichtend, da Rust nur konstante Zeichenketten (String Literals) als Formatstrings akzeptiert. Der Grund dafür ist, dass Rust zugunsten der Performance und der Stabilität die Formatstrings nicht zur Laufzeit, sondern zur Kompilierzeit parst und prüft. Dynamisch zur Laufzeit erstellte Formatstrings sind in Rust nicht möglich. Das folgende Beispiel funktioniert daher nicht:

fn main() {
    let answer = 42;
    let format_string = "The answer is {}";
    println!(format_string, answer);    // This does NOT compile because
                                        // format string needs to be
                                        // a string literal.
}

Listing 4: Keine Variablen als Formatstrings

Zwei weitere Makros, die Formatstrings akzeptieren, sind write und writeln. Sie erstellen die Ergebniszeichenfolge und schreiben sie in einen Writer. Ein Writer ist in diesem Zusammenhang eine beliebige Instanz, die eine write_fmt-Methode anbietet. In der Praxis kommt die Implementierung von write_fmt meistens aus den Traits std::fmt::Write oder std::io::Write. Letzterer wird insbesondere von Strukturen implementiert, die Dateien, Streams und Ähnliches repräsentieren. Interessanterweise implementiert aber auch die Vec-Struktur den std::io::Write-Trait, wodurch folgender Code möglich wird:

use std::io::Write;

fn main() {
    let answer = 42;

    // facts will receive the UTF8 byte of the resulting string
    let mut facts: Vec<u8> = Vec::new();
    write!(&mut facts, "The answer is {}", answer).unwrap();

    println!("{:?}", facts);
}

Listing 5: Formatstrings im write-Makro

Auch das panic-Makro kann mit Formatstrings umgehen. Das ist in der Praxis häufig relevant, um sprechende Fehlermeldungen zusammenzubauen. Hier ein Beispiel mit einer Panic, deren Nachricht mit Formatstring generiert ist:

fn main() {
    let answer = 41;
    let expected_answer = 42;

    if answer != expected_answer {
        panic!("{} is not {} - the world is going TO END :-O", answer, expected_answer);
    }

    // The output (stderr) will be:
    // thread 'main' panicked at '41 is not 42 - the world is going TO END :-O', src/main.rs:6:9
}

Listing 6: Formatstrings im panic-Makro

Die nächste Frage ist, wie die oben genannten Makros Variablenwerte in Text umwandeln. In vielen objektorientierten Programmiersprachen enthalten Objekte dafür eine virtuelle ToString-Methode, die sich überschreiben lässt. Bei Rust ist das etwas anders, es sieht dafür primär zwei Traits vor:

  • Eine Struktur, die sich in eine endbenutzertaugliche Textrepräsentation umwandeln lässt, muss explizit den Display-Trait implementieren.
  • Zum Umwandeln in eine technische Textrepräsentation (beispielsweise für Logs und zum Debuggen) muss eine Struktur den Debug-Trait implementieren. Dieser Trait lässt sich meist mit dem #[derive(Debug)]-Makro generieren.

Um genau zu sein, gibt es auch in Rust im ToString-Trait eine to_string-Methode. Der Trait ist jedoch nicht direkt einzubetten, sondern der Fokus sollte auf Display liegen. Strukturen, die Display implementieren, implementieren automatisch auchToString.

Empfohlener redaktioneller Inhalt

Mit Ihrer Zustimmmung wird hier ein externes YouTube-Video (Google Ireland Limited) geladen.

Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Google Ireland Limited) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung.

Das folgende Beispiel zeigt das Prinzip von Display, Debug und ToString:

use std::fmt::{Display, Formatter, Result};

#[derive(Debug)] // Generate Debug trait impl using derive makro
struct Person {
    first_name: String,
    last_name: String,
}

impl Display for Person {
    fn fmt(&self, f: &mut Formatter) -> Result {
        // Use write macro to generate end-user friendly
        // text representation of person instance.
        write!(f, "{}, {}", self.last_name, self.first_name)
    }
}

fn main() {
    let p = Person {
        first_name: String::from("Foo"),
        last_name: String::from("Bar"),
    };

    // Print end-user friendly person
    // Result: Bar, Foo
    println!("{}", &p);

    // Every struct that implements Display auto-implements
    // the ToString trait. It offers the to_string function that
    // turns the struct into a string.
    println!("{}", p.to_string());

    // Print technical string representation of person
    // Result: Person { first_name: "Foo", last_name: "Bar" }
    println!("{:?}", &p);
}

Listing 7: Display-, Debug- und ToString-Traits

In Listing 7 ist der Formatstring {:?} zu beachten. Er führt dazu, dass println den Debug-Trait verwendet. Um exakt zu sein, signalisiert der Doppelpunkt, dass danach eine Formatangabe folgt. Das Fragezeichen in der Formatangabe ist es, das die Debug-Ausgabe statt der endbenutzertauglichen Ausgabe auslöst. Das bringt uns zum nächsten Thema, der Ausgabeformatierung.