In Rust gibt es zwei String-Typen, nicht einen. &str (gesprochen „string slice") ist eine geborgte View auf UTF-8-Bytes — leicht, immutable, kostengünstig zu kopieren. String ist ein heap-allozierter Container, der wachsen kann, Inhalte hält, mutiert werden kann. Wer den Unterschied verinnerlicht, schreibt deutlich idiomatischeren Code: Parameter werden &str, Rückgaben oft String, und die Sprache erledigt die Konvertierungen meist transparent. Dieser Artikel zerlegt beide Typen, zeigt ihre Konvertierungen und gibt eine klare Regel für API-Design.
Die zwei Typen
&str — der geborgte View
&str ist eine Referenz auf eine Sequenz von UTF-8-Bytes mit fester Länge. Es besitzt die Daten nicht — es zeigt nur auf sie.
fn main() {
let begruessung: &str = "Hallo"; // String-Literal, &'static str
let aus_string: &str = &String::from("Welt");
println!("{begruessung}, {aus_string}!");
}Eigenschaften:
- Immutable. Du kannst die Bytes durch ein
&strnicht mutieren. - Variable Lebensdauer (Lifetime). String-Literale haben
'static— sie leben für die gesamte Programm-Laufzeit.&straus einemStringlebt nur, solange derStringlebt. - Fat Pointer im Speicher: Datenzeiger + Länge in Bytes.
Copyin einem speziellen Sinne: die Referenz selbst wird kopiert (16 Bytes auf 64-bit), die zugrundeliegenden Daten nicht.
String — der owned Container
String ist ein heap-allozierter, mutierbarer Buffer:
fn main() {
let mut s: String = String::from("Hallo");
s.push_str(", Welt");
s.push('!');
println!("{s}"); // "Hallo, Welt!"
}Eigenschaften:
- Owned. Besitzt seine Daten — droppt sie beim Verlassen des Scopes.
- Mutable mit
mut. Kann wachsen, schrumpfen, Inhalt ändern. - Heap-alloziert. Drei Felder: Pointer, Länge, Kapazität (24 Bytes auf 64-bit).
- Nicht
Copy— Move-Semantik wie beiVec<T>.
Die zentrale Faustregel
- Funktions-Parameter:
&str. Es nimmt alles auf — String-Literale,&String-Borrows, Sub-Slices. - Rückgaben und Storage:
String. Wenn du den String produzierst oder festhalten willst. - In Strukturen: meist
String. Außer du hast einen klaren Grund für einen Borrow (z. B. ein kurzlebiger Parser, der nicht überlebt).
// ✓ Akzeptiert &str-Literal UND &String — beides ist &str
fn ist_email(s: &str) -> bool {
s.contains('@')
}
// ✓ Produziert einen neuen String
fn baue_url(host: &str, pfad: &str) -> String {
format!("https://{host}{pfad}")
}
fn main() {
let host = String::from("example.com");
assert!(ist_email("user@host")); // String-Literal
assert!(ist_email(&host) || true); // &String automatisch zu &str
let url = baue_url(&host, "/api");
println!("{url}");
}Der Trick: &String lässt sich durch Deref-Coercion automatisch zu &str machen. Eine &str-Funktion akzeptiert daher beides — und du musst dich nicht für eine Variante entscheiden.
Konvertierungen
Zwischen den beiden gibt es vier Standard-Wege:
// String -> &str (kostenlos, Deref)
let s = String::from("Hallo");
let r: &str = &s;
let r2: &str = s.as_str();
// &str -> String (alloziert)
let lit = "Hallo";
let s1: String = lit.to_string();
let s2: String = String::from(lit);
let s3: String = lit.to_owned();
let s4: String = lit.into();Wann welche Variante:
.to_string()ist die universellste — funktioniert auf allem, wasDisplayimplementiert (42.to_string()ergibt"42")..to_owned()ist semantisch klarer („mache einen owned Wert daraus") — explizit für die Konvertierungs-Operation.String::from(...)ist der konstruktor-artige Stil..into()funktioniert, wenn der Ziel-Typ aus dem Kontext klar ist (Annotation, Funktions-Signatur).
Alle vier produzieren identisches Verhalten — Geschmackssache, welche du wählst.
Speicher-Layout
Im Detail sind die Typen so aufgebaut:
use std::mem::size_of;
println!("{}", size_of::<&str>()); // 16 — ptr + len
println!("{}", size_of::<String>()); // 24 — ptr + len + capacity&str: 16 Bytes (auf 64-bit), zwei Felder — Pointer auf den ersten Byte und Länge.String: 24 Bytes, drei Felder — Pointer, Länge, Kapazität. Die zusätzliche Kapazität erlaubt das Wachsen ohne Reallokation bei jedempush.
Die Bytes selbst leben nicht in diesen Strukturen — sie liegen entweder im Programm-Binary (für &'static str-Literale) oder auf dem Heap (für String).
String-Methoden
String und &str teilen sich einen Großteil ihrer API durch Deref. Was nur auf String lebt: alles, was Mutation oder Ownership voraussetzt.
let mut s = String::from("Hallo");
s.push(',');
s.push_str(" Welt!"); // String anhängen
s.insert(0, '>'); // Zeichen einfügen
s.insert_str(1, "!! "); // String einfügen
s.truncate(10); // Auf N Bytes kürzen
s.clear(); // Auf "" zurücksetzenlet s = "Hallo, Welt!";
s.len(); // 12 — Bytes
s.is_empty(); // false
s.starts_with("Hallo"); // true
s.ends_with('!'); // true
s.contains("Welt"); // true
s.find('W'); // Some(7) — byte-position
s.replace("Welt", "Erde"); // String
s.to_uppercase(); // String
s.trim(); // &str
s.split(',').collect::<Vec<_>>(); // Vec<&str>Beachte: Methoden wie to_uppercase, replace, trim().to_string() allokieren neue Strings — der ursprüngliche bleibt unverändert.
String-Literale
String-Literale ("...") sind immer &'static str — sie leben im Programm-Binary, nicht auf dem Heap, und sind unbegrenzt lange verfügbar:
let kurz: &str = "Hi";
let lang: &str = "Ein längerer Text, der trotzdem statisch ist";
// Const
const HOST: &str = "localhost";
// Static (mit Adresse)
static GLOBAL: &str = "global";Sie sind besonders günstig: kein Heap-Touch, kein Drop nötig, Kopien sind 16-Byte-Pointer-Bewegungen. Wenn dein String konstant ist, lass ihn als Literal stehen — wandele ihn nicht ohne Grund in einen String um.
Raw-Strings
Für Strings mit vielen Backslashes (Regex, Pfade auf Windows, Dateinamen) gibt es die r"..."-Syntax:
let regex = r"\d{4}-\d{2}-\d{2}";
let windows_pfad = r"C:\Users\Max\Desktop";
// Mit eingebetteten Anführungszeichen — beliebig viele # umrandend
let html = r#"<a href="https://example.com">Link</a>"#;
let mit_hash = r##"er sagte: "#das ist toll""##;In Raw-Strings haben Backslashes keine Sonderbedeutung — kein \n, kein \t. Die #-Klammern erlauben eingebettete Anführungszeichen.
Praxis: Wo str und String richtig auftauchen
HTTP-Request-Builder
Der klassische Fall: Funktion akzeptiert &str, weil sie nur liest; konstruiert intern einen String durch Konkatenation.
struct Request {
url: String,
header: Vec<(String, String)>,
}
impl Request {
fn neu(host: &str, pfad: &str) -> Request {
Request {
url: format!("https://{host}{pfad}"),
header: Vec::new(),
}
}
fn mit_header(mut self, key: &str, wert: &str) -> Request {
self.header.push((key.to_owned(), wert.to_owned()));
self
}
}
fn main() {
let req = Request::neu("api.example.com", "/users/42")
.mit_header("Accept", "application/json")
.mit_header("X-Trace", "abc123");
println!("{}", req.url);
}Parameter sind &str — der Aufrufer übergibt String-Literale ohne Allokation. Erst wenn der Wert intern festgehalten werden muss (im Vec<(String, String)>), wird to_owned() aufgerufen — und allokiert.
Logging mit Kontext
fn log(level: &str, msg: &str, kontext: &[(&str, &str)]) {
let extras: String = kontext.iter()
.map(|(k, v)| format!(" {k}={v}"))
.collect();
eprintln!("[{level}] {msg}{extras}");
}
fn main() {
log("INFO", "Anfrage verarbeitet",
&[("user", "42"), ("ms", "13")]);
}Alle Parameter sind &str oder Slices davon — die Funktion alloziert genau einen String am Ende (extras), der Rest läuft borrow-only.
Datenbank-Result-Mapping
struct User { id: u64, email: String, name: String }
fn aus_row(id: u64, email: &str, name: &str) -> User {
User {
id,
email: email.to_owned(), // Persistent — als String halten
name: name.to_owned(),
}
}Der Database-Driver liefert &str als View auf den internen Puffer — der Buffer wird beim nächsten Query überschrieben. Also: in den Struct hinein als String konvertieren.
Config-Wert lesen, ohne zu modifizieren
struct Config<'a> {
db_host: &'a str,
db_port: u16,
}
impl<'a> Config<'a> {
fn aus_env(buf: &'a str) -> Option<Config<'a>> {
let mut host = None;
let mut port = None;
for line in buf.lines() {
if let Some(v) = line.strip_prefix("DB_HOST=") { host = Some(v); }
if let Some(v) = line.strip_prefix("DB_PORT=") { port = v.parse().ok(); }
}
Some(Config { db_host: host?, db_port: port? })
}
}Hier ist &str mit Lifetime perfekt — die Config zeigt direkt in den Original-Buffer, keine Allokation. Solange der Buffer lebt, lebt die Config. Mehr zu Lifetimes im eigenen Kapitel.
Interessantes
&String als Parameter ist fast immer ein Code-Smell.
Wenn du fn foo(s: &String) schreibst, schränkst du Aufrufer ohne Grund ein — String-Literale müssten erst in String konvertiert werden, was alloziert. Mit fn foo(s: &str) funktioniert beides. Clippy warnt mit ptr_arg.
String ist im Grunde Vec mit UTF-8-Invariante.
Wer String::into_bytes() aufruft, bekommt den unterliegenden Vec<u8>. Wer String::from_utf8(vec) aufruft, prüft, ob die Bytes wohlgeformt UTF-8 sind. Diese Invariante ist die einzige strukturelle Schutzschicht — unsafe Methoden wie from_utf8_unchecked umgehen sie auf eigene Gefahr.
String-Literale leben im Programm-Binary, nicht auf dem Heap.
"Hallo" ist eine &'static str-Referenz auf Bytes im Read-only-Datensegment des Binaries. Kein Drop, kein Clone allokiert, keine Heap-Aktivität. Wer einen String nicht modifiziert, sollte ihn als Literal stehen lassen.
String wächst exponentiell — Reallokationen sind amortisiert günstig.
Beim push_str wird, sobald die Kapazität nicht reicht, der Buffer in der Größe verdoppelt. Das macht viele kleine push-Operationen insgesamt O(n) — wie bei Vec. Bei sehr großen Strings lohnt es sich trotzdem, mit String::with_capacity(n) initial die Größe zu reservieren.
to_string(), to_owned(), String::from und .into() sind funktional gleich.
Alle vier produzieren denselben Maschinencode. Wahl nach Lesbarkeit. Mein persönlicher Stil: .to_string() bei numerischen Werten (42.to_string()), .to_owned() bei &str-Konvertierung ("x".to_owned()).
Raw-Strings sind ideal für Regex und Pfade.
Regex-Patterns wie \d{4}-\d{2}-\d{2} sehen in raw-Schreibweise (r"\d{4}-\d{2}-\d{2}") wie im Regex-Standard aus. Ohne raw müsste man jeden Backslash verdoppeln, was schnell zu Fehlern führt.
Strings sind in Rust UTF-8 — nicht UTF-16 wie in JavaScript oder Java.
Das vermeidet die Surrogate-Probleme dieser Sprachen. „Anzahl der Zeichen" ist trotzdem mehrdeutig (Bytes vs. chars vs. Grapheme). Mehr im Iteration-Artikel.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Strings
- std::string::String
- std::primitive.str
- Clippy –
ptr_arg-Lint - Rust API Guidelines – Naming