Im Strings-Kapitel hast du String als Text-Typ kennengelernt — dieser Artikel betrachtet ihn aus Container-Perspektive: ein wachsender Buffer auf dem Heap, der intern wie ein Vec<u8> arbeitet, aber die zusätzliche UTF-8-Garantie durchsetzt. Du lernst die Container-API (push, push_str, insert, truncate, clear), das Kapazitäts-Modell (with_capacity, reserve, shrink_to_fit) und die Beziehung zu Vec<u8>. Wer im Hot-Path Strings baut, profitiert massiv vom richtigen Kapazitäts-Management.
String = Vec<u8> mit UTF-8-Invariante
Wenn man die Strings-Kapitel-Artikel als Einführung ins UTF-8-Handling versteht, ist dieser hier die Container-Sicht: was ist String unter der Haube, und welche API teilt er mit Vec? Die Antwort ist überraschend einfach: String ist nichts anderes als ein Vec<u8> mit einer zusätzlichen Invariante — die Bytes müssen jederzeit gültiges UTF-8 bilden. Alles Andere — Wachstumsstrategie, Speicher-Layout, Move-Semantik — ist identisch.
use std::mem::size_of;
fn main() {
println!("{}", size_of::<String>()); // 24 (ptr + len + cap)
println!("{}", size_of::<Vec<u8>>()); // 24 — identisch
}Drei Felder auf dem Stack (24 Bytes auf 64-bit), funktional identisch zu denen eines Vec<u8>. Der ptr zeigt auf den ersten UTF-8-Byte im Heap; die len zählt die belegten Bytes (nicht die Zeichen — das ist eine häufige Verwechslung); die capacity ist der reservierte Platz im Heap-Block, der gleich oder größer als len ist.
Die UTF-8-Garantie wird nicht durch Laufzeit-Prüfungen erzwungen, sondern durch die API-Oberfläche: alle Methoden, die in den Buffer schreiben (push, push_str, insert, insert_str), akzeptieren nur Eingaben, die selbst schon UTF-8-konform sind (char, &str). Wer mit roh-Bytes einsteigt — etwa aus einer Datei oder dem Netzwerk —, muss durch String::from_utf8 gehen, das eine vollständige Validierung durchführt. Diese Trennung erlaubt es, im normalen Code völlig unbedenklich Strings zu manipulieren, ohne dass Validierungs-Kosten anfallen, weil das System mathematisch garantiert, dass der String niemals ungültiges UTF-8 enthält.
Konstruktion
Die Konstruktion eines String hat ein paar Eigenheiten, die du kennen solltest. Anders als bei Vec, wo ein leerer Vec keine Heap-Allokation auslöst, ist das bei String genauso — String::new() reserviert null Bytes auf dem Heap. Die Wahl der Konstruktor-Form hängt davon ab, was du in der Hand hast (Literal, Bytes, geschätzte Größe) und ob du dem System eine Performance-Information mitgeben willst.
fn main() {
// Leer
let s1 = String::new();
// Aus String-Literal
let s2 = String::from("Hallo");
let s3 = "Hallo".to_string();
// Mit Kapazität
let s4 = String::with_capacity(1024);
// Aus Vec<u8> (mit UTF-8-Prüfung)
let bytes = vec![72u8, 97, 108, 108, 111]; // "Hallo"
let s5 = String::from_utf8(bytes).unwrap();
}String::from(literal) und literal.to_string() sind funktional äquivalent — beide allozieren einen neuen Heap-Block, kopieren die Bytes des Literals hinein und liefern einen besitzten String. Der einzige Unterschied ist Lesbarkeit: String::from macht klarer, dass hier ein neuer Container entsteht; to_string() reiht sich besser in Method-Chain-Notation ein. Beide sind in Hot-Paths messbar schnell, aber nicht null — bei jeder Konstruktion wird Heap-Speicher reserviert.
String::with_capacity(n) ist der wichtigste Performance-Hebel. Wenn du in einem Loop einen String aufbaust und ungefähr weißt, wie groß er werden wird, sparst du dir alle Reallocations — der einmal reservierte Block trägt die ganze Operation. Im Praxis-Abschnitt unten siehst du das Pattern mehrfach.
String::from_utf8(bytes) ist der Eingangspunkt, wenn du rohe Bytes in einen String konvertieren willst. Die Methode validiert die gesamten Bytes und gibt Result<String, FromUtf8Error> zurück — bei invalidem UTF-8 bekommst du nicht nur den Fehler, sondern auch die ursprünglichen Bytes zurück, sodass du sie noch aufräumen oder anders verarbeiten kannst. Die Validierung ist O(n) im Umfang der Bytes; bei sehr großen Buffern entsprechend teuer.
Anfügen und Einfügen
Beim Wachsen eines Strings hast du vier Hauptmethoden, und welche du wählst, hängt davon ab, was du einfügst (einzelnes Zeichen oder Zeichenfolge) und wo (am Ende oder mittendrin). Anhängen am Ende ist immer O(1) amortisiert, weil hinter dem letzten Zeichen nichts liegt, das verschoben werden müsste. Einfügen in der Mitte ist O(n), weil die nachfolgenden Bytes nach hinten geschoben werden müssen — analog zu Vec::insert.
let mut s = String::from("Hallo");
s.push(','); // einzelnes char
s.push_str(" Welt!"); // String-Slice anhängen
s.insert(0, '>'); // char einfügen
s.insert_str(1, " Hi "); // String einfügen
// s ist jetzt "> Hi Hallo, Welt!"push(char) hängt ein einzelnes Unicode-Zeichen an. Wichtig: ein char in Rust ist immer ein Unicode-Code-Point, nicht ein Byte — er kann beim UTF-8-Encoding 1 bis 4 Bytes belegen. push('ö') schreibt zwei Bytes (0xC3 0xB6), push('🦀') vier. Du musst dir um die Encoding-Details nichts kümmern; die Methode kennt UTF-8 und schreibt korrekt.
push_str(&str) ist der häufigste Fall: einen Slice anhängen. Da der Slice schon UTF-8 ist (das garantiert der &str-Typ), reduziert sich die Operation auf einen memcpy der Bytes ans Ende des Buffers. Bei großer Differenz zwischen len und capacity ist das eine einzige schnelle Operation; muss vorher der Buffer wachsen, kommen die Reallocation-Kosten dazu.
insert(i, char) und insert_str(i, &str) sind die Mittendrin-Varianten. Hier liegt eine wichtige Stolperfalle: der Index i ist ein Byte-Index, kein Zeichen-Index. Wenn du in einem String mit Mehrbyte-Zeichen wie "Müll" an Position 2 einfügen willst, muss diese Position eine char-Grenze sein — sonst panickt der Aufruf mit „byte index is not a char boundary". Diese Eigenheit ist eine direkte Folge der UTF-8-Garantie: Rust kann nicht erlauben, dass du mitten in ein Mehrbyte-Zeichen schreibst, weil das den String invalidieren würde. Bei rein-ASCII-Strings ist Byte-Index gleich Zeichen-Index, dann ist die Sache unkritisch.
Format-Macro für Konkatenation
Wenn du einen String mit eingebetteten Werten bauen willst, ist format! die idiomatische Wahl. Das Makro funktioniert genauso wie println! — nur dass das Ergebnis nicht ausgegeben, sondern als String zurückgegeben wird. Die Syntax mit benannten Captures ({name} für Variablen im Scope) macht das Schreiben kompakt und lesbar.
let name = "Anna";
let alter = 28;
let s = format!("{name} ist {alter} Jahre alt");
// s: String, alloziert.Intern ist format! ein Compile-Zeit-Makro: es expandiert zu einem String::with_capacity(geschätzte_Größe) plus einer Reihe von write!-Aufrufen, die die Argumente formatieren und direkt in den Buffer schreiben. Du bekommst genau eine Allokation für das gesamte Resultat, kein Schritt-für-Schritt-Wachstum. Bei sehr eng getakteten Hot-Paths (z. B. innerer Loop eines Renderers) lohnt sich allerdings die noch sparsamere Variante write!(&mut buffer, ...) mit einem wiederverwendeten Buffer, die ganz ohne Allocation pro Aufruf auskommt.
Entfernen und Kürzen
Beim Verkleinern eines Strings gibt es vier Hauptoperationen, jeweils mit unterschiedlichem Wirkungsbereich. Alle haben gemeinsam, dass sie die UTF-8-Invariante respektieren — sie panicken lieber, als einen invaliden String zurückzulassen.
let mut s = String::from("Hallo, Welt!");
s.truncate(5); // "Hallo"
s.pop(); // Some('o'), s = "Hall"
s.clear(); // ""
let mut s = String::from("Hallo, Welt!");
s.remove(5); // entfernt ',' an Byte-Position 5truncate(n) kürzt den String auf maximal n Bytes. Wenn der String schon kürzer war, ist die Operation ein No-Op; wenn n mitten in ein Mehrbyte-Zeichen fällt, panickt es. Die Operation ist O(1) — sie schiebt nur den len-Wert nach unten und droppt eventuell die ehemals hinteren Bytes (was bei u8 nichts macht). Die Kapazität bleibt unangetastet.
pop() entfernt das letzte Zeichen (nicht das letzte Byte) und gibt es als Option<char> zurück. Bei einem leeren String ist das None. Intern muss pop den letzten char-Boundary finden (also bei UTF-8 zurückwandern, bis ein Lead-Byte erkannt wird), das ist aber O(1) — maximal 4 Schritte.
clear() setzt len = 0 und droppt alle Zeichen, behält aber die Kapazität. Dies ist die Grundlage für Buffer-Reuse: in einem Loop kannst du immer wieder denselben String befüllen und mit clear zurücksetzen, ohne dass eine einzige neue Allocation entsteht — solange die neuen Inhalte in die ursprüngliche Kapazität passen.
remove(i) löscht das Zeichen an Byte-Position i und schiebt alle folgenden Bytes nach vorne — O(n). Auch hier muss i an einer char-Grenze liegen, sonst Panic.
retain — filterndes Entfernen
let mut s = String::from("a1b2c3d4");
s.retain(|c| c.is_alphabetic());
assert_eq!(s, "abcd");retain ist die in-place-Filter-Operation für Strings. Der String wird einmal durchlaufen, alle Zeichen, für die das Prädikat true liefert, werden behalten — die übrigen herausgeworfen. Wichtig ist das Wort in-place: es entsteht kein zweiter Buffer, die übrigen Zeichen werden im selben Heap-Block nach vorne verschoben. Damit ist die Operation O(n), aber ohne neue Allokation, was sie deutlich billiger macht als s.chars().filter(...).collect().
Kapazitäts-Management
Das Kapazitäts-Verhalten von String ist identisch zu Vec — Verdopplung bei Bedarf, with_capacity zum Vor-Reservieren, reserve für Laufzeit-Hints, shrink_to_fit zum Schrumpfen. Der Grund dafür wurde oben schon erwähnt: String ist ja intern ein Vec<u8> mit zusätzlicher Invariante. Trotzdem lohnt sich ein eigener Blick, weil String in der Praxis häufig in Loops mit vielen kleinen Puffern verwendet wird, wo das Kapazitäts-Spiel besonders zählt.
let mut s = String::with_capacity(100);
println!("{} / {}", s.len(), s.capacity()); // 0 / 100
s.push_str("Hallo");
println!("{} / {}", s.len(), s.capacity()); // 5 / 100
s.reserve(1000); // mindestens 1000 weitere
println!("{} / {}", s.len(), s.capacity()); // 5 / >= 1005
s.shrink_to_fit(); // Kapazität auf len reduzieren
println!("{} / {}", s.len(), s.capacity()); // 5 / 5Im Beispiel siehst du die drei wichtigsten Operationen. with_capacity(100) startet mit 100 Bytes reserviertem Speicher — der String ist leer, hat aber sofort Platz für 100 Bytes ohne Wachstum. reserve(1000) stellt sicher, dass nach dem Aufruf mindestens 1000 zusätzliche Bytes ohne Reallocation hineinpassen — der Allocator darf mehr nehmen, aber nicht weniger. shrink_to_fit gibt überflüssigen Heap-Speicher zurück und reduziert die Kapazität auf die aktuelle Länge.
Die Anwendung in der Praxis: with_capacity ist die Standard-Wahl, wenn du zu Beginn weißt, wie groß der String werden wird; reserve ist die Laufzeit-Variante, wenn du im Loop neue Information bekommst („jetzt kommen 5000 zusätzliche Bytes"); shrink_to_fit ist sinnvoll bei langlebigen Strings, deren finale Größe deutlich kleiner ist als die Spitzen-Kapazität — etwa bei einem String, den du erst sehr groß baust und dann zurechtkürzt.
Konvertierung zu Vec<u8>
Da String und Vec<u8> strukturell identisch sind, sind Konvertierungen zwischen beiden besonders billig — oft sogar O(1). Du brauchst sie regelmäßig: bei Datei-I/O, Netzwerk-Protokollen, Verschlüsselungs-APIs, Bild-Verarbeitung. Die Stdlib bietet drei Wege, die sich nach Borrow oder Move und nach Validierung oder Vertrauen unterscheiden.
let s = String::from("Hallo");
// Borrow als &[u8]:
let bytes: &[u8] = s.as_bytes();
// Verbrauch zu Vec<u8>:
let v: Vec<u8> = s.into_bytes();
// s ist hier nicht mehr nutzbar.
// Zurück (mit UTF-8-Prüfung):
let zurueck = String::from_utf8(v).unwrap();as_bytes() gibt eine geliehene &[u8] zurück, die auf denselben Heap-Block zeigt wie der String. Es findet keine Allokation, keine Kopie statt — du bekommst einen Read-Only-View. Solange der Borrow lebt, kannst du den String nicht ändern; das ist die normale Borrow-Checker-Mechanik.
into_bytes() ist eine Move-Konvertierung: der String wird konsumiert, die drei Header-Felder (ptr, len, cap) werden zu einem Vec<u8> umgedeutet — die eigentlichen Bytes auf dem Heap werden nicht angefasst. Die Operation ist daher O(1), egal wie groß der String ist. Danach ist die String-Bindung weg, der Vec hat den Besitz.
String::from_utf8(vec) ist der Rückweg: ein Vec<u8> soll als String interpretiert werden. Da ein beliebiger Byte-Vec nicht garantiert gültiges UTF-8 enthält, prüft die Methode alle Bytes und liefert Result<String, FromUtf8Error>. Bei einem invalid byte sequence wird ein Fehler zurückgegeben, der den ursprünglichen Vec mitliefert — so kannst du ihn anders verarbeiten oder loggen. Die Validierung ist O(n) auf den Bytes; bei MB-großen Vecs entsprechend teuer.
Unsafe-Variante
let bytes = vec![72, 97, 108, 108, 111];
// Wenn du SICHER bist, dass die Bytes UTF-8 sind:
let s = unsafe { String::from_utf8_unchecked(bytes) };from_utf8_unchecked umgeht die Validierung komplett — der Vec wird ohne Prüfung als String interpretiert. Die Operation ist O(1) und damit deutlich schneller als die geprüfte Variante, aber sie ist unsafe: wenn die Bytes nicht gültiges UTF-8 sind, hast du einen invaliden String erzeugt, und jede weitere Operation darauf (Iteration über chars, Slicing, Debug-Ausgabe) ist Undefined Behavior. Der Anwendungsfall ist eng: du nutzt sie nur, wenn du logisch garantieren kannst, dass die Bytes gültig sind — etwa weil sie aus einer Quelle stammen, die UTF-8 beweisbar erzeugt (z. B. der Output einer anderen Rust-String-API, die du gerade serialisiert hast). In allen anderen Fällen ist from_utf8 die richtige Wahl, und der Overhead der Validierung ist es wert.
Praxis: String als Buffer im echten Code
In realem Code ist String selten nur ein passiver Text-Container; viel öfter ist er ein Buffer, den du in einem Loop befüllst, leerst, befüllst — oder eine Zielsammlung, in die du Werte hineinformatierst. Die folgenden Patterns sind die häufigsten, die du in Bibliotheken, Web-Servern, Parsern, Renderern und CLI-Tools wiederfindest. Bei jedem wird die Kapazitäts-Strategie zur Schlüssel-Entscheidung.
Effizientes Konkatenieren
pub fn baue_csv_zeile(felder: &[&str]) -> String {
let gesamt_groesse: usize = felder.iter().map(|f| f.len() + 1).sum();
let mut zeile = String::with_capacity(gesamt_groesse);
for (i, f) in felder.iter().enumerate() {
if i > 0 { zeile.push(','); }
zeile.push_str(f);
}
zeile
}Dieses Beispiel zeigt die maximal effiziente Form des String-Builds. Bevor irgendetwas geschrieben wird, summiert eine kurze Iteration die exakte Endgröße auf (Längen der Felder plus ein Trennzeichen pro Feld). Dann wird mit with_capacity der genau passende Heap-Block reserviert. Im eigentlichen Build-Loop kommen push und push_str zum Einsatz, ohne dass eine einzige Reallocation ausgelöst wird. Bei einer CSV-Zeile mit 100 Feldern à durchschnittlich 20 Bytes wären das ohne Vor-Reservierung ca. 7-8 Wachstumsschritte; die Größenvorberechnung kostet eine zweite Iteration, ist aber bei langen Feldern trotzdem schneller.
Stream-Reader-Pattern
use std::io::{self, BufRead};
pub fn lese_zeilen<R: BufRead>(reader: R) -> io::Result<Vec<String>> {
let mut zeilen = Vec::new();
let mut buffer = String::with_capacity(256);
let mut r = reader;
while r.read_line(&mut buffer)? > 0 {
zeilen.push(buffer.clone());
buffer.clear();
}
Ok(zeilen)
}Das Stream-Reader-Pattern ist klassisch: der BufRead::read_line-Methode wird ein mutable String-Buffer übergeben, in den die nächste Zeile geschrieben wird. Anschließend wird der Buffer geklont (in den Ergebnis-Vec) und durch clear zurückgesetzt — bei einer 10 000-zeiligen Datei spart das 9 999 Allocations. read_line arbeitet additiv: bestünde das clear nicht, würde jede neue Zeile an die vorhergehende drangehängt. Die initiale Kapazität von 256 Bytes ist eine pragmatische Schätzung für typische Log-/CSV-Zeilen; bei sehr breiten Zeilen wächst der Buffer einmal beim ersten Lesen, danach reicht er für alle folgenden.
HTML-Escape mit Buffer
pub fn baue_liste_html(items: &[&str]) -> String {
let mut html = String::with_capacity(items.len() * 32);
html.push_str("<ul>\n");
for item in items {
html.push_str(" <li>");
html.push_str(item);
html.push_str("</li>\n");
}
html.push_str("</ul>");
html
}HTML-Generierung ist ein Lehrbuch-Anwendungsfall für String-as-Builder. Statt die Liste über String-Konkatenation mit + aufzubauen (was bei jeder Operation eine neue Allokation auslöst), wird ein einziger Buffer befüllt. Die Schätzung items.len() * 32 ist großzügig — sie geht von durchschnittlich 32 Bytes pro Listenpunkt inklusive Tags aus. Bei kürzeren Items verschwendest du etwas Speicher, bei längeren sind ein, zwei Wachstumsschritte zu erwarten. Beide Szenarien sind besser, als gar nicht zu reservieren. Beim Generieren großer HTML-Dokumente (etwa tausende Tabellenzeilen) ist der Performance-Unterschied zur naiven Konkatenations-Variante drei- bis fünffach.
URL-Encoding
pub fn url_encode(input: &str) -> String {
let mut result = String::with_capacity(input.len());
for c in input.chars() {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => {
result.push(c);
}
_ => {
for byte in c.to_string().bytes() {
result.push_str(&format!("%{byte:02X}"));
}
}
}
}
result
}URL-Encoding ist ein gutes Beispiel, weil hier Char-für-Char-Verarbeitung gefragt ist — manche Zeichen passieren unverändert, andere müssen in %XX-Hex-Sequenzen umgewandelt werden. Die Kapazitäts-Schätzung input.len() ist eine Untergrenze: im Best Case (alle Zeichen erlaubt) trifft sie exakt, im Worst Case (alle Zeichen müssen escaped werden) verdreifacht sich die Länge. Die fünf zugelassenen Zeichen-Bereiche (a-z, A-Z, 0-9, -, _, ., ~) stammen aus RFC 3986 — sie sind die „unreserved characters", die nicht escapet werden müssen. Für Nicht-ASCII-Zeichen geht der Code über c.to_string() und iteriert die UTF-8-Bytes; das ist nicht die effizienteste, aber die lesbarste Form.
Log-Eintrag-Format
pub fn formatiere_log(level: &str, kontext: &[(&str, &str)], msg: &str) -> String {
let mut s = String::with_capacity(128 + msg.len());
s.push('[');
s.push_str(level);
s.push_str("] ");
s.push_str(msg);
if !kontext.is_empty() {
s.push_str(" {");
for (i, (k, v)) in kontext.iter().enumerate() {
if i > 0 { s.push_str(", "); }
s.push_str(k);
s.push('=');
s.push_str(v);
}
s.push('}');
}
s
}Strukturierte Log-Formatierung ist ein typischer Anwendungsfall für hand-gepflegte String-Builds. Die Schätzung 128 + msg.len() ist konservativ — sie reserviert genug Platz für den Header (Level, Klammern, Kontext-Marker) und addiert die Nachricht. Wenn der Kontext sehr groß ist, kann ein einzelner Wachstumsschritt nötig werden, aber die häufigen Log-Einträge ohne Kontext bleiben in einer Allokation. Diese Art von Buffer-Build ist klassisches Material für structured logging — wenn dein Service Millionen Logs pro Stunde schreibt, summieren sich die gesparten Allocations zu beträchtlichen Speicher- und Latenz-Einsparungen.
Filterndes Cleanup mit retain
pub fn entferne_kommentare(input: &str) -> String {
let mut result = input.to_string();
result.retain(|c| c != '#');
result
}Dieses Beispiel zeigt retain von der pragmatischen Seite: ein bestehender Text wird übernommen und in-place gefiltert. Die Alternative input.chars().filter(...).collect::<String>() würde einen komplett neuen String allozieren — bei langen Texten doppelt so viel Heap-Verbrauch während der Operation. Der to_string()-Aufruf vorher ist unvermeidlich, weil retain einen &mut String braucht, nicht ein &str. Wenn der Aufrufer schon einen String besitzt, kannst du diesen direkt entgegennehmen und sparst auch diese Allokation.
Buffer-Reuse mit clear
pub fn process_many<F: Fn(&str)>(inputs: &[&str], handler: F) {
let mut buffer = String::with_capacity(1024);
for input in inputs {
buffer.clear();
buffer.push_str("[normalisiert] ");
buffer.push_str(input.trim());
handler(&buffer);
}
}Das Buffer-Reuse-Pattern ist die effizienteste Form, viele kleine String-Operationen in Sequenz durchzuführen. Eine einzige Allokation am Anfang trägt den gesamten Loop. Bei 10 000 Inputs sparst du 10 000 Allocations und ebenso viele Heap-Freigaben. Voraussetzung ist, dass der Callback den Buffer nicht persistent festhält — sondern ihn nur innerhalb des Aufrufs konsumiert. Wenn der Callback den Inhalt speichern muss, braucht er einen eigenen String::from(&buffer)-Klon; das hebt einen Teil der Einsparung wieder auf, behält aber den Vorteil des stabilen Read-Buffers.
String aus Iterator bauen
pub fn slug_aus_woertern(woerter: &[&str]) -> String {
woerter.iter()
.map(|w| w.to_lowercase())
.collect::<Vec<_>>()
.join("-")
}Dieses Beispiel kombiniert zwei Patterns: zuerst map zum Transformieren (Lowercase), dann collect::<Vec<_>> zum Materialisieren, schließlich join("-") als Separator-Insertion. Die Variante mit Zwischenvektor ist explizit und gut lesbar — sie produziert einen Vec mit n Strings und konkateniert ihn dann mit Trennzeichen. Eine alternative Variante ohne Zwischenvektor wäre woerter.iter().map(|w| w.to_lowercase()).collect::<Vec<_>>().join("-") — identisch zum gezeigten Code. Für sehr lange Listen kann es lohnen, mit String::with_capacity und manueller push_str-Schleife zu arbeiten; bei kurzen Listen ist die join-Form klar idiomatisch.
Numerische Formatierung
pub fn formatiere_groesse_bytes(bytes: u64) -> String {
let units = ["B", "KB", "MB", "GB", "TB"];
let mut size = bytes as f64;
let mut unit_idx = 0;
while size >= 1024.0 && unit_idx < units.len() - 1 {
size /= 1024.0;
unit_idx += 1;
}
format!("{size:.2} {}", units[unit_idx])
}Die formatiere_groesse_bytes-Funktion ist ein typischer Helper, den fast jede CLI hat — Bytes als „1.50 MB" statt „1572864" ausgeben. Der Loop teilt jedes Mal durch 1024 und steigt eine Einheit hoch, bis das Resultat unter 1024 liegt oder die größte Einheit (TB) erreicht ist. format!("{size:.2} {}", ...) nutzt das Genauigkeits-Modifikator :.2 für zwei Nachkommastellen. Die Funktion alloziert einen String pro Aufruf — bei seltenen Aufrufen (CLI-Ausgabe einmal pro Minute) völlig egal, bei millionenfacher Verwendung im Hot-Path würde man stattdessen write!(&mut shared_buffer, ...) mit einem geteilten Buffer nutzen, um die Allokation zu sparen.
Interessantes
String ist strukturell Vec mit UTF-8-Garantie.
Gleiche Heap-Allocation, gleiche Drei-Felder-Struktur, gleiche Wachstums-Strategie. Der einzige Unterschied: jeder Schreibvorgang stellt sicher, dass das Resultat weiterhin gültiges UTF-8 ist.
String::with_capacity ist Performance-kritisch.
Wenn du in einem Loop einen String aufbaust und die finale Größe kennst (oder grob schätzen kannst), spart with_capacity alle Reallocations. Bei 1000-Element-Build-Loops ist das oft 10x schneller.
format! alloziert einmal.
format!("{a}{b}{c}") macht intern eine einzige Allocation, die für das gesamte Ergebnis reicht — der Compiler schätzt die Größe basierend auf den Format-Argumenten. Effizienter als manuelles Konkatenieren mit +.
push_str ist günstiger als + format!.
Klassisches s = s + format!(...) macht zwei Allocations (eine für format!, eine für die Konkatenation). s.push_str(&format!(...)) wäre eine Allocation. Aber write!(&mut s, "...", ...) ist null Extra-Allocations.
truncate und remove panicken bei Multi-Byte-Mitte.
Wie beim normalen String-Slicing: Byte-Index muss an char-Grenze sein. Bei UTF-8-Strings vorher prüfen mit is_char_boundary oder Methoden wie chars/char_indices nutzen.
clear() behält die Kapazität — perfekt für Loops.
Wer einen String in einem Loop wiederverwendet (buffer.clear(); buffer.push_str(...)), bekommt nur eine initiale Allocation. Sehr typisches Stream-Reader-Pattern.
shrink_to_fit reduziert die Kapazität.
Wenn du einen String länger im Speicher hältst und dessen finale Größe deutlich kleiner als die Kapazität ist: shrink_to_fit() gibt überschüssigen Heap-Speicher frei. Macht eine Re-Allocation — also nicht in Hot-Paths.
into_bytes ist Move, kein Copy.
String::into_bytes() ist O(1) — der String wird zum Vec<u8> umgepoolt, die Bytes bleiben am gleichen Heap-Block. Keine Kopier-Operation.