Kapitel 05 (Fortgeschritten): Architektur & Profi-Techniken mit Zeichenketten
Dieses Kapitel richtet sich an Entwickler, die Rust in produktiven Umgebungen einsetzen und hochperformante, speichereffiziente und flexible APIs entwerfen möchten. Zeichenketten gehören in fast jeder Anwendung zu den am häufigsten genutzten Datentypen. Umso wichtiger ist es, ihre Speicherarchitektur zu verstehen und zu beherrschen.
Item 6: Bevorzuge &str als Funktionsargument zur Erhöhung der API-Flexibilität
Wenn Sie Funktionen entwerfen, die Text als Eingabe verarbeiten, stehen Sie oft vor der Frage, welchen Typ das Argument haben sollte. Die naheliegende Wahl für viele Einsteiger ist der Typ String. Für Bibliotheken und APIs ist dies jedoch in den meisten Fällen eine Einschränkung der Flexibilität und führt zu unnötigen Performance-Kosten.
Die Alltagsanalogie: Das Vergrößerungsglas
Stellen Sie sich vor, Sie betreiben ein Fotoalbum-Kopierstudio. Wenn ein Kunde Ihnen ein Foto zeigt, das Sie in ein Album einfügen sollen, haben Sie zwei Möglichkeiten:
- Besitzübertragung (
String): Sie verlangen das Originalbild und behalten es. Wenn der Kunde das Foto später noch für sich selbst haben möchte, muss er vor dem Besuch bei Ihnen eine teure Kopie des Bildes anfertigen lassen (entspricht.clone()auf dem Heap). - Sicht auf das Original (
&str): Sie werfen einfach nur einen Blick durch ein Vergrößerungsglas auf das Bild des Kunden. Der Kunde behält das Foto, und Sie können die Bilddaten trotzdem auslesen und verarbeiten.
In Rust ist &str dieses Vergrößerungsglas. Es ermöglicht den Zugriff auf den Text, ohne dass der Speicher kopiert oder der Besitz übertragen werden muss.
Das Prinzip der Deref Coercion
Rust bietet einen Mechanismus namens Deref Coercion (automatische Typumwandlung durch Entreferenzierung). Dieser Mechanismus greift ein, wenn Sie eine Referenz auf einen Typ T an eine Funktion übergeben, die eine Referenz auf einen Typ U erwartet, sofern T das Trait Deref<Target = U> implementiert.
Da String das Trait std::ops::Deref mit dem Zieltyp str implementiert, gilt Folgendes:
#![allow(unused)]
fn main() {
// Vereinfachte Darstellung der Implementierung in der Standardbibliothek:
impl Deref for String {
type Target = str;
fn deref(&self) -> &str {
// Gibt eine Sicht auf das zugrundeliegende Byte-Array zurück
&self[..]
}
}
}
Wenn Sie nun eine Referenz auf einen String (Typ &String) an eine Funktion übergeben, die einen &str erwartet, wandelt der Compiler die Referenz automatisch und ohne Laufzeitkosten in einen &str um.
Kompilierbares Beispiel
Das folgende Beispiel zeigt, wie eine einzige Funktion dank &str verschiedene String-Typen akzeptieren kann, ohne dass Daten auf dem Heap dupliziert werden müssen:
// Diese API ist maximal flexibel. Sie akzeptiert jeden Typ,
// der sich als String-Slice darstellen lässt.
fn print_message(message: &str) {
println!("Nachricht empfangen: {}", message);
}
fn main() {
// Fall 1: Ein klassisches String-Literal (Typ: &'static str)
// Literale liegen direkt im schreibgeschützten Datensegment des Binärprogramms.
let literal: &'static str = "Hallo, statische Welt!";
print_message(literal);
// Fall 2: Ein dynamischer String auf dem Heap (Typ: String)
let dynamic_string: String = String::from("Hallo, dynamische Welt!");
// Wir übergeben eine Referenz auf den String (&String).
// Dank Deref Coercion konvertiert Rust dies automatisch in &str.
print_message(&dynamic_string);
// Fall 3: Ein Teilausschnitt (Slice) des dynamischen Strings
// Wir übergeben nur die Zeichen von Index 7 bis 17.
let slice: &str = &dynamic_string[7..17];
print_message(slice);
}
Zeilenweise Erklärung des Codes:
- Zeile 3: Die Funktion
print_messagedeklariert den Parametermessageals&str. Dadurch signalisiert sie: “Ich benötige nur Lesezugriff auf den Text und beanspruche keinen Besitz.” - Zeile 10:
literalzeigt direkt auf ein vordefiniertes Literal im statischen Programmspeicher. Es wird kein Heap-Speicher allokiert. - Zeile 14:
dynamic_stringallokiert Speicher auf dem Heap, um den Text dynamisch verwalten zu können. - Zeile 18:
print_message(&dynamic_string)zeigt die Magie der Deref Coercion. Obwohl&dynamic_stringden Typ&Stringhat, kompiliert der Code fehlerfrei, da Rust im Hintergrunddynamic_string.deref()aufruft, um den passenden&strzu erhalten.
Typischer Compilerfehler und Behebung
Wenn Sie eine Funktion fälschlicherweise so definieren, dass sie den Besitz eines String erzwingt, obwohl sie den Text nur lesen möchte, schränken Sie die Verwendbarkeit stark ein.
// Fehlerhafte API-Definition
fn verarbeite_nachricht(nachricht: String) {
println!("Verarbeite: {}", nachricht);
}
fn main() {
let literal = "Mein Text";
// COMPILER-FEHLER:
// verarbeite_nachricht(literal);
// expected struct `String`, found `&str`
}
Warum lehnt der Compiler das ab?
Der Compiler lehnt diesen Aufruf ab, weil ein &str (ein einfacher Zeiger mit Längenangabe) nicht die Besitzanforderungen eines String-Objekts erfüllt. Ein String besitzt seinen Speicher auf dem Heap und ist für dessen Freigabe verantwortlich. Um die Funktion aufzurufen, müssten Sie das Literal explizit umwandeln (z.B. mit .to_string()), was eine teure Heap-Allokation zur Folge hätte.
Behebung:
Ändern Sie die Funktionssignatur von nachricht: String zu nachricht: &str. Dadurch entfallen alle Allokationskosten beim Aufruf mit Literalen oder bereits existierenden Zeichenketten.
Item 7: Minimiere Heap-Allokationen durch Vorab-Dimensionierung von String-Kapazitäten
Ein String in Rust ist intern als Vektor von Bytes (Vec<u8>) implementiert. Er speichert UTF-8-kodierten Text auf dem Heap. Da sich die Länge des Strings zur Laufzeit ändern kann, verwaltet Rust den Speicher dynamisch. Das unbedachte Vergrößern eines Strings in Schleifen kann jedoch zu massiven Performance-Einbußen führen.
Die Alltagsanalogie: Der Umzugskarton
Stellen Sie sich vor, Sie ziehen um und besitzen 50 Bücher.
- Der ineffiziente Weg: Sie kaufen zuerst einen winzigen Karton, in den genau ein Buch passt. Sie legen das erste Buch hinein. Um das zweite Buch einzupacken, müssen Sie einen neuen Karton kaufen, in den zwei Bücher passen. Sie holen das erste Buch aus dem alten Karton, legen beide Bücher in den neuen Karton und werfen den alten Karton weg. Diesen Vorgang wiederholen Sie für jedes einzelne Buch. Der Arbeitsaufwand ist gigantisch.
- Der effiziente Weg: Sie wissen im Voraus, dass Sie 50 Bücher haben. Sie gehen zum Laden und kaufen direkt einen großen Karton, der Platz für 50 Bücher bietet. Sie packen alle Bücher nacheinander ein, ohne jemals den Karton wechseln zu müssen.
In Rust entspricht die Größe des Kartons der Kapazität (capacity) des Strings, während die Anzahl der aktuell eingepackten Bücher der Länge (len) entspricht.
Speicher-Reallokation im Detail
Ein String besteht auf dem Stack aus drei Datenfeldern (jeweils mit der Breite eines Systemzeigers, also insgesamt 24 Bytes auf 64-Bit-Systemen):
- Pointer (
ptr): Zeigt auf die Startadresse des Speicherbereichs auf dem Heap. - Length (
len): Die Anzahl der aktuell belegten UTF-8-Bytes. - Capacity (
cap): Die Gesamtzahl der Bytes, die auf dem Heap für diesen String reserviert wurden.
Wenn Sie mit .push() oder .push_str() Text an einen String anhängen, prüft Rust, ob len nach dem Anhängen größer als cap wäre. Ist dies der Fall, tritt eine Reallokation auf:
- Rust fordert vom Betriebssystem (bzw. dem Speichermanager) einen neuen, meist doppelt so großen Speicherbereich auf dem Heap an.
- Die bestehenden Bytes werden an die neue Adresse kopiert.
- Der alte Speicherbereich wird freigegeben.
- Der Zeiger
ptrwird auf die neue Adresse umgebogen undcapwird aktualisiert.
Diese Schritte sind extrem teuer, da sie Systemaufrufe und Speicherkopieroperationen beinhalten.
Kompilierbares Beispiel
Das folgende Beispiel demonstriert den Performance-Vorteil von String::with_capacity() beim Aufbau einer großen Zeichenkette:
fn main() {
// ----------------------------------------------------
// Ineffizienter Ansatz: Ständige Reallokation
// ----------------------------------------------------
let mut ineffizienter_string = String::new();
println!("Start-Kapazität (neu): {}", ineffizienter_string.capacity()); // Gibt 0 aus
for i in 0..5 {
ineffizienter_string.push_str("Messwert;");
// Wir beobachten, wie die Kapazität schrittweise wächst und Reallokationen auslöst
println!("Durchlauf {}: Kapazität = {}", i, ineffizienter_string.capacity());
}
// ----------------------------------------------------
// Effizienter Ansatz: Vorab-Reservierung
// ----------------------------------------------------
// Wir berechnen im Voraus: 5 Iterationen * 9 Bytes ("Messwert;") = 45 Bytes.
// Wir runden zur Sicherheit leicht auf.
let mut effizienter_string = String::with_capacity(50);
println!("\nStart-Kapazität (optimiert): {}", effizienter_string.capacity()); // Garantiert >= 50
for _ in 0..5 {
effizienter_string.push_str("Messwert;");
// Die Kapazität bleibt über die gesamte Schleife hinweg konstant
println!("Optimiert - Kapazität: {}", effizienter_string.capacity());
}
}
Zeilenweise Erklärung des Codes:
- Zeile 6:
String::new()erstellt einen leeren String ohne Allokation auf dem Heap (capacity == 0). - Zeile 9: In jedem Schleifendurchlauf wird Text angehängt. Da die Kapazität anfangs 0 ist, muss Rust sofort Speicher allokieren und bei weiteren Überschreitungen mehrmals vergrößern.
- Zeile 20:
String::with_capacity(50)teilt dem Speichermanager sofort mit, dass wir 50 Bytes Speicher auf dem Heap benötigen. Die Allokation findet genau einmal statt. - Zeile 23: Da alle angehängten Daten in den reservierten Puffer passen, bleibt die Kapazität stabil und es finden keine teuren Kopierprozesse statt.
Item 8: Verwende FromStr zur Standardisierung von String-Parsing für eigene Domänentypen
Das Parsen von Zeichenketten in strukturierte Daten ist eine der häufigsten Aufgaben in der Softwareentwicklung. In Rust gibt es dafür eine standardisierte Schnittstelle: das Trait std::str::FromStr. Sobald Sie dieses Trait für Ihre eigenen Typen implementieren, können Sie die universelle Methode .parse() der Standardbibliothek nutzen.
Die Alltagsanalogie: Der Paket-Scanner
Stellen Sie sich ein Logistikzentrum vor. Auf dem Förderband kommen unzählige Pakete an, die mit unstrukturierten Text-Etiketten beklebt sind. Manche Etiketten sind beschädigt oder unleserlich.
Ein automatischer Paket-Scanner (FromStr) liest die rohe Zeichenkette. Entspricht sie dem erwarteten Format (z. B. “PLZ-Straße-Hausnummer”), wandelt er sie in eine strukturierte Lieferroute um (den Domänentyp). Wenn das Etikett fehlerhaft ist, sortiert der Scanner das Paket aus und gibt eine Fehlermeldung aus, damit das Problem manuell behoben werden kann.
Das FromStr-Trait und die Fehlerbehandlung
Das Trait FromStr ist wie folgt definiert:
#![allow(unused)]
fn main() {
pub trait FromStr: Sized {
type Err;
fn from_str(s: &str) -> Result<Self, Self::Err>;
}
}
Zwei Aspekte sind hier architektonisch von Bedeutung:
type Err(Assoziierter Typ): Sie müssen festlegen, welcher Fehlertyp zurückgegeben wird, wenn das Parsen fehlschlägt. Verwenden Sie hierfür niemalsString, sondern definieren Sie präzise Enums, die es dem Aufrufer erlauben, programmatisch auf verschiedene Fehlerursachen zu reagieren.Result<Self, Self::Err>: Die Methode gibt niemals panischunwrap()aufgerufen zurück. Sie nutzt dasResult-Muster, um Fehler sicher an den Aufrufer zu melden. Innerhalb der Implementierung können Sie den?-Operator verwenden, um Zwischenfehler elegant weiterzuleiten.
Kompilierbares Beispiel: Ein IPv4-Adressen-Parser
Wir implementieren einen Parser für eine IPv4-Adresse, die aus vier Oktetten (Bytes) besteht, getrennt durch Punkte (z. B. "192.168.1.1").
use std::str::FromStr;
// Unser strukturierter Domänentyp
#[derive(Debug, PartialEq, Eq)]
pub struct IPv4Address {
pub octets: [u8; 4],
}
// Unser präziser Fehlertyp für das Parsing
#[derive(Debug, PartialEq, Eq)]
pub enum ParseIPError {
InvalidFormat, // Zu viele oder zu wenige Punkte
InvalidOctet, // Ein Wert ist keine Zahl oder außerhalb des Bereichs 0-255
}
// Die Implementierung des standardisierten FromStr-Traits
impl FromStr for IPv4Address {
type Err = ParseIPError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// 1. Text an den Punkten aufteilen
let parts: Vec<&str> = s.split('.').collect();
// Eine IPv4-Adresse muss exakt aus 4 Teilen bestehen
if parts.len() != 4 {
return Err(ParseIPError::InvalidFormat);
}
// Puffer für die geparsten Bytes
let mut octets = [0u8; 4];
// 2. Jedes Oktett einzeln parsen
for i in 0..4 {
// parse() auf &str gibt ein Result<u8, ParseIntError> zurück.
// Wir wandeln diesen Fehler mit map_err in unseren ParseIPError um
// und leiten ihn im Fehlerfall mit dem ? Operator sofort weiter.
let parsed_octet = parts[i]
.parse::<u8>()
.map_err(|_| ParseIPError::InvalidOctet)?;
octets[i] = parsed_octet;
}
// 3. Erfolgreiches Ergebnis zurückgeben
Ok(IPv4Address { octets })
}
}
fn main() {
// Dank der FromStr-Implementierung können wir die parse()-Methode direkt auf Slices nutzen!
let ip_str = "192.168.178.1";
// Die Typinferenz von Rust weiß, dass wir eine IPv4Address erwarten
match ip_str.parse::<IPv4Address>() {
Ok(ip) => println!("Erfolgreich geparst: {:?}", ip),
Err(e) => println!("Parsing fehlgeschlagen: {:?}", e),
}
// Beispiel für ein ungültiges Format
let kaputt = "192.168.1";
assert_eq!(kaputt.parse::<IPv4Address>(), Err(ParseIPError::InvalidFormat));
}
Zeilenweise Erklärung des Codes:
- Zeile 4:
IPv4Addresskapselt ein Array mit 4 Bytes (u8). Dies stellt sicher, dass ungültige Werte wie negative Zahlen oder Werte über 255 gar nicht erst im Typ gespeichert werden können. - Zeile 17: Wir legen fest, dass das Trait für
IPv4Addressimplementiert wird. - Zeile 18:
type Err = ParseIPError;verknüpft unseren Fehlertyp mit dem Trait. - Zeile 22:
s.split('.')erzeugt einen Iterator über die Teilstücke. Wir sammeln sie in einem Vektor. - Zeile 25: Falls die Anzahl der Segmente nicht genau 4 beträgt, brechen wir sofort ab und geben das Resultat
Err(ParseIPError::InvalidFormat)zurück. - Zeile 35: Hier nutzen wir
.parse::<u8>()für jedes Segment. Da diese Methode bei Fehlschlag einenParseIntErrorder Standardbibliothek zurückgibt, passen wir diesen mit.map_err(...)an unser eigenes Fehler-Enum an. Das?sorgt dafür, dass die Funktion im Fehlerfall sofort beendet wird und der Fehler nach oben gereicht wird.
Typischer Compilerfehler und Behebung
Ein häufiger Fehler bei der Implementierung von FromStr ist das Vergessen des assoziierten Typs Err oder das Verwenden einer falschen Signatur.
#![allow(unused)]
fn main() {
impl FromStr for IPv4Address {
// COMPILER-FEHLER: missing associated type `Err`
fn from_str(s: &str) -> Result<Self, Self::Err> {
// ...
}
}
}
Behebung:
Sie müssen zwingend type Err = ...; innerhalb des impl-Blocks definieren. Rust benötigt diesen Typ zur Kompilierzeit, um die Signatur der Methode from_str vollständig aufzulösen.
Item 9: Optimiere Lebenszeiten mit Box::leak für langlebige globale Konfigurations-Slices
In Rust ist das concept der Lebenszeiten (Lifetimes) allgegenwärtig. Manchmal benötigen Sie im gesamten Programm Zugriff auf Konfigurationsdaten (z. B. eine Datenbank-URL), die erst zur Laufzeit aus einer Datei oder Umgebungsvariablen gelesen werden. Der naive Ansatz, diese Daten als Referenz durch alle Strukturen zu reichen, führt zu komplexen Lifetime-Annotationen (<'a>). Die Standardbibliothek bietet mit Box::leak eine elegante Lösung, um zur Laufzeit statischen Speicher zu erzeugen.
Die Alltagsanalogie: Das Denkmal auf dem Marktplatz
Stellen Sie sich vor, Sie leihen ein Buch aus der Bibliothek aus. Sie dürfen es nur für eine begrenzte Zeit behalten (temporäre Lebenszeit). Wenn Sie das Buch an einen Freund weitergeben möchten, müssen Sie sicherstellen, dass er es zurückgibt, bevor Ihre Leihfrist abläuft. Das erfordert ständige Absprachen und Koordination (Lifetime-Annotationen).
Wenn Sie das Buch jedoch kaufen, es mit Zement übergießen und fest auf dem Marktplatz einbetonieren (Speicher leaken), bleibt es dort für immer stehen ('static). Jedes Mitglied der Gemeinschaft kann jederzeit zu diesem Buch gehen und es lesen, ohne sich jemals um Fristen oder Rückgaben sorgen zu müssen.
Funktionsweise von Box::leak
Normalerweise wird der Speicher einer Heap-Allokation (Box<T>) automatisch freigegeben, sobald die Box den Gültigkeitsbereich verlässt (durch das Trait Drop).
Die Funktion Box::leak nimmt eine Box entgegen, umgeht den automatischen Destruktor und gibt eine veränderbare Referenz mit der Lebenszeit 'static (&'static mut T) zurück. Der Speicher wird somit dauerhaft aus der Verwaltung des Heap-Sammlers entlassen. Er bleibt an einer festen Adresse im Speicher erhalten, bis das Programm beendet wird.
Kompilierbares Beispiel
Das folgende Beispiel zeigt, wie Sie eine zur Laufzeit geladene Konfiguration in eine 'static str Referenz umwandeln, um sie einfach global zu nutzen:
use std::collections::HashMap;
// Eine globale Struktur, die eine statische Referenz auf die Konfiguration hält.
// Dadurch müssen wir keine komplexen Lifetimes an der Struktur deklarieren!
struct ConfigManager {
connection_string: &'static str,
}
fn lade_konfiguration_aus_umgebung() -> &'static str {
// 1. Wir simulieren das Auslesen eines Werts zur Laufzeit.
// Dieser String wird dynamisch auf dem Heap allokiert.
let raw_input: String = String::from("postgres://admin:secret@localhost:5432/prod_db");
// 2. Speicheroptimierung: Wir wandeln den String in eine Box<str> um.
// String besitzt oft zusätzliche Kapazitäts-Reserven auf dem Heap.
// into_boxed_str() gibt diesen überschüssigen Speicher frei und schrumpft
// die Allokation auf die exakte Größe des Texts.
let boxed_str: Box<str> = raw_input.into_boxed_str();
// 3. Statischer Leak: Wir leaken den Speicher dauerhaft.
// Box::leak gibt uns eine &'static str Referenz zurück.
let static_ref: &'static str = Box::leak(boxed_str);
static_ref
}
fn main() {
// Konfiguration zur Laufzeit erzeugen
let db_url: &'static str = lade_konfiguration_aus_umgebung();
// Der ConfigManager benötigt keine Lebenszeit-Parameter,
// da &'static str das gesamte Programm über gültig bleibt!
let manager = ConfigManager {
connection_string: db_url,
};
println!("Datenbank-URL im Manager: {}", manager.connection_string);
}
Zeilenweise Erklärung des Codes:
- Zeile 12:
raw_inputist un dynamischerString. Seine Lebenszeit ist auf die Funktionlade_konfiguration_aus_umgebungbeschränkt. - Zeile 18:
into_boxed_str()ist ein wichtiger Optimierungsschritt. EinStringhat oft eine größere Kapazität als Länge.Box<str>hingegen belegt auf dem Heap exakt die Byte-Breite des Textes. Dadurch wird kein Speicherplatz verschwendet. - Zeile 22:
Box::leak(boxed_str)ist der entscheidende Systemaufruf. Er teilt Rust mit: “Lösche diesen Speicherbereich nicht, wenn die Funktion endet. Ich übernehme die Verantwortung dafür, dass er bis zum Programmende existiert.” - Zeile 33: Wir erstellen den
ConfigManager. Damanager.connection_stringdie Lebenszeit'staticbesitzt, kann die Struktur im gesamten Programm herumgereicht werden, ohne dass wir Lifetime-Parameter wieConfigManager<'a>deklarieren müssen.
Warning
Architektonischer Warnhinweis:
Box::leaksollte ausschließlich für Daten verwendet werden, die einmalig beim Programmstart initialisiert werden und über die gesamte Programmlaufzeit benötigt werden. Wenn SieBox::leakin Schleifen oder bei periodisch wiederkehrenden Events aufrufen, erzeugen Sie ein unkontrolliertes Speicherleck (Memory Leak), welches das Programm nach einiger Zeit wegen Speichermangels abstürzen lässt.
Item 10: Implementiere Display für maßgeschneiderte, performante String-Formatierung von Domänentypen
Rust unterscheidet strikt zwischen zwei Formatierungs-Traits für die Textausgabe:
std::fmt::Debug(Platzhalter{:?}): Für Entwickler zur Fehlersuche. Zeigt interne Details des Typs. Kann fast immer über#[derive(Debug)]automatisch generiert werden.std::fmt::Display(Platzhalter{}): Für Endanwender. Zeigt eine schön formatierte, benutzerfreundliche Textdarstellung. Muss manuell implementiert werden.
Für eine professionelle API ist die saubere Implementierung von Display unerlässlich, um eigene Domänentypen nahtlos in das Formatierungs-Ökosystem von Rust (z.B. println!, format!, write!) zu integrieren.
Die Alltagsanalogie: Der Dolmetscher
Stellen Sie sich vor, Sie haben eine Struktur, die eine Uhrzeit speichert (z. B. Sekunden seit Mitternacht).
- Die Bäcker-Sicht (
Debug): Zeigt Ihnen die Rohdaten:Uhrzeit { sekunden: 45296 }. Das hilft Ihnen beim Debuggen des Programmcodes. - Die Dolmetscher-Sicht (
Display): Übersetzt diese Rohdaten für den Hotelgast auf der Speisekarte in ein gut lesbares Format:12:34:56.
Display ist dieser Dolmetscher. Er übersetzt die internen Datenstrukturen in eine für Menschen verständliche Sprache.
Zero-Allocation-Formatierung
Ein zentraler Vorteil des Display-Traits in Rust ist seine hohe Performance. Die Methode fmt arbeitet direkt mit einem Formatter (Typ &mut fmt::Formatter). Wenn Sie innerhalb der Implementierung das Makro write! verwenden, werden die formatierten Daten direkt in den Zielpuffer geschrieben (z. B. direkt in den Standard-Ausgabekanal der Konsole oder in eine Datei).
Es wird kein temporärer Zwischen-String auf dem Heap allokiert, wie es in vielen anderen Sprachen der Fall ist (wo oft erst ein String zusammengesetzt und dann zurückgegeben wird).
Kompilierbares Beispiel
Wir erweitern unseren in Item 8 erstellten Typ IPv4Address um eine ansprechende Display-Implementierung.
use std::fmt;
#[derive(Debug)] // Debug-Trait wird automatisch generiert (für {:?})
pub struct IPv4Address {
pub octets: [u8; 4],
}
// Manuelle Implementierung von Display (für {})
impl fmt::Display for IPv4Address {
// Die Methode nimmt eine unveränderliche Referenz auf sich selbst (&self)
// und eine veränderliche Referenz auf den Formatter-Stream (f) entgegen.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Das write! Makro schreibt die Daten formatiert in den Stream 'f'.
// Wir verwenden kein Semikolon am Ende, da wir das Resultat von write!
// (das ein fmt::Result ist) direkt als Rückgabewert der Funktion nutzen.
write!(
f,
"{}.{}.{}.{}",
self.octets[0], self.octets[1], self.octets[2], self.octets[3]
)
}
}
fn main() {
let ip = IPv4Address {
octets: [192, 168, 1, 1],
};
// 1. Ausgabe über Debug (technisch)
println!("Debug-Ausgabe: {:?}", ip);
// Ausgabe: Debug-Ausgabe: IPv4Address { octets: [192, 168, 1, 1] }
// 2. Ausgabe über Display (benutzerfreundlich)
println!("Display-Ausgabe: {}", ip);
// Ausgabe: Display-Ausgabe: 192.168.1.1
// 3. Verwendung im format!-Makro
let s = format!("Standard-Gateway ist: {}", ip);
assert_eq!(s, "Standard-Gateway ist: 192.168.1.1");
}
Zeilenweise Erklärung des Codes:
- Zeile 8: Wir implementieren das Trait
fmt::Display. Im Gegensatz zuDebugkann dieses Trait nicht mit#[derive]generiert werden, da der Compiler nicht wissen kann, welches Layout für den Endbenutzer gewünscht ist. - Zeile 11: Die Methode
fmtgibt einfmt::Resultzurück. Dieses signalisiert, ob der Schreibvorgang erfolgreich war oder ob der Ziel-Stream (z. B. eine volle Festplatte) das Schreiben blockiert hat. - Zeile 14: Das Makro
write!verhält sich genau wieformat!, schreibt die Bytes jedoch direkt in den Streamf. Durch die Rückgabe des Resultats vonwrite!(ohne abschließendes Semikolon) leiten wir eventuelle Schreibfehler automatisch weiter. - Zeile 31:
println!("Display-Ausgabe: {}", ip)sucht im Hintergrund nach derDisplay-Implementierung fürIPv4Addressund führt unsere Methode aus.