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

Seite 2: Beispiele für die Ausgabeformatierung in der Praxis

Inhaltsverzeichnis

In der Praxis ist es häufig notwendig, Variablenwerte, die sich mit Hilfe eines Formatstrings in eine Zeichenkette umwandeln lassen, entsprechend zu formatieren. Beispiele, die über die oben bereits erwähnte Debug-Ausgabe hinaus gehen, sind:

  • Padding, also das Auffüllen auf eine gewünschte Ausgabelänge
  • Auswahl des gewünschten Zahlensystems (wie hexadezimal oder binär) bei numerischen Datentypen
  • Anzahl der Ziffern nach dem Komma bei Gleitkommawerten
  • Ausgabe der Adresse eines Pointers statt des Speicherinhalts, auf den er referenziert

Eine vollständige Beschreibung aller Parameter für Formatstrings würde den Rahmen dieses Artikels sprengen – zumal sich auch in der Rust-Dokumentation eine genaue Beschreibung aller Möglichkeiten nachschlagen lässt. Das folgende Beispiel zeigt daher nur das Prinzip der Ausgabeformatierung. Es berechnet einen Zapfen (oder einen Turm, wie es in Österreich heißt), eine beliebte Übung zum Lernen von Multiplizieren und Dividieren in der Grundschule. Dabei geht man von einer Basiszahl aus und multipliziert sie mit 2, dann mit 3, mit 4 und so weiter. Wenn man bei der gewünschten Höhe angelangt ist, dividiert man durch 2, dann durch 3 und so fort – schließlich muss wieder die Basiszahl herauskommen. Am Ende des Listings ist ein Beispiel für einen Zapfen dargestellt.

/// Calculates the length of a given, positive number
fn len_of_positive_number(mut value: i32) -> usize {
    let mut len = 0;
    while value > 0 {
        len += 1;
        value /= 10;
    }

    len
}

fn main() {
    let start_value = 5;
    let height = 6;
    let len_height = len_of_positive_number(height);

    let mut value = start_value;
    for op in ['*', '/'] {
        for i in 2..=height {
            let new_value = match op {
                '*' => value * i,
                '/' => value / i,
                _ => panic!("Invalid op"),
            };
            // Use format string parameters to align numbers properly (padding left/right).
            // Note that length of third format string parameter is given
            // as a dynamic, usize value ($ postfix to positional parameter).
            println!("{:>9} {} {:<4$} = {}", value, op, i, new_value, len_height);
            value = new_value;
        }
    }

    /*
    The output will be:
        5 * 2 = 10
       10 * 3 = 30
       30 * 4 = 120
      120 * 5 = 600
      600 * 6 = 3600
     3600 / 2 = 1800
     1800 / 3 = 600
      600 / 4 = 150
      150 / 5 = 30
       30 / 6 = 5
    */
}

Listing 8: Formatierung von Variablenwerten

In der Praxis gibt es oft mehrere Variablen, deren Werte Entwicklerinnen und Entwickler mit Hilfe des Formatstrings in die Ergebniszeichenkette einbauen möchten. Auch für das mehrfache Einfügen ein und derselben Variable besteht realer Bedarf. Für solche Fälle sehen Formatstrings in Rust Positional- und Named-Parameter vor. Das folgende Beispiel stellt die beiden Ansätze gegenüber:

fn main() {
    let answer = 42;

    // Positional parameter
    println!("{} is the answer so {0} is a great number", answer);

    // Note: Named parameter
    println!("{a} is the answer so {a} is a great number", a = answer);
}

Listing 9: Parameter referenzieren

Wie eingangs erwähnt, hat die Rust-Version 1.58 (erschienen im Januar 2022) einen neuen Trick in Sachen Referenzieren von Parametern gelernt: Captured Identifiers in Format Strings. Man kann jetzt Variablen mit ihrem Namen referenzieren, ohne sie nach dem Formatstring explizit als Parameter angeben zu müssen. Listing 10 veranschaulicht neben der richtigen Verwendung auch die wesentliche Einschränkung, dass sich aktuell nur Identifier, aber keine Expressions im Formatstring einsetzen lassen.

#[derive(Debug)]
struct Person {
    first_name: String,
    last_name: String,
}

fn main() {
    // Captured identifiers in format strings
    let answer = 42;
    println!("{answer} is the answer so {answer} is a great number");

    let answer = 41;
    // Note that the following line would NOT compile. You can
    // only capture identifier, you cannot use expressions. Use
    // a helper variable or other parameter forms (positional or named)
    // if you want to use expressions.
    //println!("{answer + 1} is the answer so {answer + 1} is a great number");

    // Captured identifiers also support output formatting.
    // In this case, we use the Debug trait to get a technical string
    // representation of Person.
    let p = Person {
        first_name: String::from("Foo"),
        last_name: String::from("Bar"),
    };
    println!("Content of p: {p:?}");
}

Listing 10: Captured Identifiers in Format Strings

Obwohl wie erwähnt Rust die Formatstrings nicht zur Laufzeit parst, sind format und println oft nicht die besten Optionen, wenn es um performancekritischen Code geht. Sind beispielsweise einfach nur Zeichenketten aneinander zu hängen, gibt es schnellere Alternativen. Auf GitHub finden sich Beispielprojekte mit Benchmarks, die verschiedene Möglichkeiten gegenüberstellen. Da sich Rust ständig weiterentwickelt, ist es empfehlenswert, sich nicht blind auf die genannten Ergebnisse zu verlassen, sondern projektspezifische Benchmarks zu schreiben.

Ich möchte jedoch davor warnen, Formatstrings generell zu meiden und immer zu versuchen, das letzte Quäntchen Performance durch individuell optimierte Algorithmen zur Erstellung von Zeichenketten zu verwenden. Die Lesbarkeit und Wartbarkeit des Codes nimmt dadurch ab. Der individuelle Weg sollte dort eingeschlagen werden, wo Laufzeitperformance wirklich das entscheidende Kriterium ist.