&str ist der wichtigste Slice-Typ in Rust — und der Typ, den 90 % aller Funktions-Parameter haben, wenn sie mit Text arbeiten. Er ist eine Referenz auf einen UTF-8-validierten Byte-Bereich: garantiert wohlgeformt, garantiert kontigent, garantiert keine Mutation. String-Literale ("Hallo") haben den Typ &'static str, ein &String wird via Deref-Coercion zu &str, und ein Sub-Bereich eines anderen &str ist wieder &str. Dieser Artikel zeigt den Typ in allen Facetten, geht durch die wichtigsten Methoden und macht das API-Pattern „nimm &str, nicht &String" wasserdicht.
Was &str ist
Auf Speicher-Ebene ist &str ein Fat Pointer:
use std::mem::size_of;
fn main() {
let s: &str = "Hallo";
println!("{}", size_of::<&str>()); // 16 — Pointer + Länge
println!("{}", s.len()); // 5 (Bytes)
}Zwei Felder, je 8 Bytes auf 64-bit:
- Pointer auf den ersten Byte des UTF-8-Bereichs.
- Länge in Bytes.
Wichtig: die Länge ist in Bytes, nicht in Zeichen. UTF-8 hat variable Byte-Längen pro Code-Point — "Müller".len() ist 7, nicht 6.
Die UTF-8-Garantie
&str darf zur Compile-Zeit nur aus garantiert UTF-8-konformen Bytes konstruiert werden. Ein &str, der jemals invalides UTF-8 enthalten würde, ist Undefined Behavior — die einzige Möglichkeit dafür führt durch unsafe. Praktisch heißt das: jede str-Methode darf annehmen, dass die Bytes wohlgeformt sind.
Drei Quellen für &str
Ein &str kann aus drei Quellen entstehen:
1. String-Literale (Default)
let s: &'static str = "Hallo";
// Die Bytes liegen im Read-Only-Datensegment des Binaries.
// 'static heißt: lebt für die gesamte Programm-Laufzeit.String-Literale haben immer den Typ &'static str. Sie sind im Binary eingebaut, kein Heap, keine Allocation.
2. Borrow von String
let owned: String = String::from("Hallo");
let s1: &str = &owned; // Deref-Coercion: &String → &str
let s2: &str = owned.as_str(); // explizit
let s3: &str = &owned[..]; // Range-IndexDrei syntaktische Varianten, alle gleichbedeutend. &owned ist die idiomatischste — sie nutzt Auto-Deref-Coercion.
3. Sub-Slice eines anderen &str
fn main() {
let s = "Hallo, Welt!";
let begruessung: &str = &s[..5]; // "Hallo"
let komma: &str = &s[5..6]; // ","
let suffix: &str = &s[7..]; // "Welt!"
}Range-Indexierung erzeugt einen neuen &str, der auf dieselben Bytes zeigt. Keine Allocation — nur ein neuer Pointer + Länge.
UTF-8-Grenzen beachten
fn main() {
let s = "Müller"; // 7 Bytes, 6 Code-Points
let teil = &s[..1]; // "M" — ok, M ist 1 Byte
// let kaputt = &s[..2]; // Panic — Grenze mitten im ü
let mit_ue = &s[..3]; // "Mü" — ok, ü endet bei Byte 3
}Slicing mitten in einem Multi-Byte-Zeichen führt zu Runtime-Panic. Im Slicing-Artikel des Strings-Kapitels ist das Thema ausführlich behandelt.
Die wichtigsten str-Methoden
Längen und Prüfungen
let s = "Hallo, Welt!";
s.len(); // 12 (Bytes)
s.is_empty(); // false
s.chars().count(); // 12 (Code-Points)
s.starts_with("Hallo"); // true
s.ends_with('!'); // true
s.contains("Welt"); // trueIteration
let s = "abc";
for b in s.bytes() { print!("{b} "); } // 97 98 99
for c in s.chars() { print!("{c} "); } // a b c
for (i, c) in s.char_indices() {
println!("{i}: {c}"); // 0: a / 1: b / 2: c
}
for line in "Eins\nZwei".lines() {
println!("{line}");
}
for w in "Hallo Welt".split_whitespace() {
println!("{w}");
}Splitting
let s = "a,b,c,d";
let teile: Vec<&str> = s.split(',').collect();
// teile = ["a", "b", "c", "d"]
let csv_with_quoting = "a,\"b,c\",d";
let einfach: Vec<&str> = csv_with_quoting.splitn(2, ',').collect();
// ["a", "\"b,c\",d"] — splitn(2, ...) maximal 2 Teile
if let Some((key, wert)) = "host=localhost".split_once('=') {
println!("{key} → {wert}"); // host → localhost
}Suche und Position
let s = "Hallo, Welt!";
s.find('W'); // Some(7) — Byte-Position
s.rfind(','); // Some(5) — von hinten
s.find("Welt"); // Some(7)
s.find(char::is_uppercase); // Some(0) — Predikat
// strip_prefix / strip_suffix für robuste Präfixe
"Hallo, Welt!".strip_prefix("Hallo, "); // Some("Welt!")
"log.txt".strip_suffix(".txt"); // Some("log")Trimming
let s = " Hallo, Welt! ";
s.trim(); // "Hallo, Welt!"
s.trim_start(); // "Hallo, Welt! "
s.trim_end(); // " Hallo, Welt!"
"###Hallo###".trim_matches('#'); // "Hallo"Transformation (gibt neuen String zurück)
let s = "Hallo, Welt!";
let upper: String = s.to_uppercase(); // alloziert
let lower: String = s.to_lowercase();
let neu: String = s.replace("Welt", "Erde");
let wiederholt: String = s.repeat(3);Wichtig: alle diese Methoden allokieren einen neuen String. Wer das vermeiden will, nutzt Iteratoren über chars()/bytes() und schreibt direkt in einen Output-Buffer.
Parsing
let n: i32 = "42".parse().unwrap();
let f: f64 = "3.14".parse().unwrap();
let result: Result<u8, _> = "999".parse(); // Err — überschreitet u8&str vs. String — die API-Regel
Die klarste Regel im gesamten Rust-Ökosystem:
- Funktions-Parameter:
&str— akzeptiert Literale,&String, Sub-Slices. - Storage (Struct-Feld, Vec-Element):
String— wenn der String festgehalten oder mutiert werden soll. - Rückgabe: meist
String— wenn ein neuer Wert produziert wird.&strzurückzugeben funktioniert nur mit Lifetime-Verknüpfung zum Input.
// ✅ Idiomatisch
pub fn ist_email(s: &str) -> bool {
s.contains('@')
}
// ❌ Unnötig einschränkend
pub fn ist_email_schlecht(s: &String) -> bool {
s.contains('@')
}
// ✅ Mit Rückgabe (alloziert)
pub fn normalisieren(s: &str) -> String {
s.trim().to_lowercase()
}
// ✅ Mit Rückgabe als Slice (lifetime-gebunden)
pub fn ohne_quotes(s: &str) -> &str {
s.trim_matches('"')
}Clippy warnt mit clippy::ptr_arg, wenn du &String als Parameter nutzt.
Raw-String-Literale
Für Strings mit vielen Backslashes oder Anführungszeichen gibt es die r"..."-Form:
let regex = r"\d{4}-\d{2}-\d{2}"; // \d wird wörtlich genommen
let pfad = r"C:\Users\Max\Desktop";
// Mit eingebetteten Anführungszeichen — beliebig viele #
let html = r#"<a href="https://example.com">Link</a>"#;
let komplex = r##"er sagte: "#nicht-interpoliert""##;In Raw-Strings haben Backslashes keine Sonderbedeutung — kein \n, kein \t. Die #-Klammern erlauben eingebettete Anführungszeichen.
Lifetimes bei &str
Eine &str hat immer eine Lifetime. Drei häufige Formen:
// 'static — lebt für die Programm-Laufzeit
let s: &'static str = "im Binary";
// 'a — gebunden an einen anderen Wert
fn ersten_chars<'a>(s: &'a str) -> &'a str {
&s[..1.min(s.len())]
}
// Implizit (Elision) — gleiche Lifetime wie Input
fn klein(s: &str) -> &str {
s.trim()
}In den meisten Fällen schließt der Compiler die Lifetime automatisch. Explizite Annotationen brauchst du erst, wenn die Inferenz nicht eindeutig ist — Details im Lifetimes-Kapitel.
Praxis: &str im echten Code
Library-Validator
pub fn ist_gueltige_url(s: &str) -> bool {
s.starts_with("http://") || s.starts_with("https://")
}
pub fn ohne_protokoll(url: &str) -> &str {
url.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))
.unwrap_or(url)
}
fn main() {
assert!(ist_gueltige_url("https://example.com"));
assert_eq!(ohne_protokoll("https://example.com/api"), "example.com/api");
}Pure &str-API — keine Allocation, kein Owned-Move. Aufrufer behält den Original-String.
Header-Parsing
pub fn parse_header(zeile: &str) -> Option<(&str, &str)> {
let (name, wert) = zeile.split_once(':')?;
Some((name.trim(), wert.trim()))
}
fn main() {
let zeile = "Content-Type: text/html; charset=utf-8";
if let Some((n, w)) = parse_header(zeile) {
println!("{n} → {w}");
}
}split_once plus trim — beide arbeiten auf &str und geben &str zurück. Zero-Copy, zero-allocation.
CSV-Zeile parsen
pub fn parse_csv_zeile(zeile: &str) -> Vec<&str> {
zeile.split(',').map(str::trim).collect()
}
fn main() {
let raw = " 42, Berlin, 2026-05-20 ";
let felder = parse_csv_zeile(raw);
assert_eq!(felder, vec!["42", "Berlin", "2026-05-20"]);
}Vec<&str> — der Vec besitzt die Slice-Pointer, die Daten leben weiter im Original-raw-String.
Multi-Line-Parser
pub fn parse_config(text: &str) -> Vec<(&str, &str)> {
text.lines()
.filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with('#'))
.filter_map(|l| l.split_once('='))
.map(|(k, v)| (k.trim(), v.trim()))
.collect()
}
fn main() {
let cfg = "
# Server
host = localhost
port = 8080
";
let entries = parse_config(cfg);
assert_eq!(entries, vec![("host", "localhost"), ("port", "8080")]);
}Eine ganze Konfigurations-Datei in Key-Value-Paare zerlegt — ohne eine einzige Allocation der Strings selbst.
URL-Pfad und Query-String trennen
pub fn split_pfad_query(url: &str) -> (&str, Option<&str>) {
match url.find('?') {
Some(pos) => (&url[..pos], Some(&url[pos + 1..])),
None => (url, None),
}
}
fn main() {
let url = "/users/42?include=email";
let (pfad, query) = split_pfad_query(url);
assert_eq!(pfad, "/users/42");
assert_eq!(query, Some("include=email"));
}find('?') gibt eine garantiert gültige Byte-Grenze zurück — Slicing ist sicher.
Log-Level-Filter
pub fn ist_fehler_zeile(zeile: &str) -> bool {
let trimmed = zeile.trim_start();
["[ERROR", "[FATAL", "[CRIT"].iter()
.any(|prefix| trimmed.starts_with(prefix))
}
pub fn extrahiere_message(zeile: &str) -> Option<&str> {
zeile.find(']').map(|pos| zeile[pos + 1..].trim_start())
}Filter und Extraktion auf &str — typisch für Log-Verarbeitung in Pipelines.
Word-Count
use std::collections::HashMap;
pub fn zaehle_woerter(text: &str) -> HashMap<&str, u32> {
let mut counts = HashMap::new();
for wort in text.split_whitespace() {
*counts.entry(wort).or_insert(0) += 1;
}
counts
}Die HashMap-Keys sind &str-Borrows in den Input-Text — keine Heap-Allocation pro Wort. Lifetime der Map ist an Input gebunden.
Tag-Renderer
pub fn tag_zeile(tags: &[&str]) -> String {
tags.iter()
.map(|t| format!("#{t}"))
.collect::<Vec<_>>()
.join(" ")
}
fn main() {
let tags = ["rust", "tutorial", "ownership"];
assert_eq!(tag_zeile(&tags), "#rust #tutorial #ownership");
}Funktion nimmt einen &[&str] — Slice von String-Slices. Sehr flexibel: akzeptiert sowohl Arrays von Literalen als auch dynamisch gebaute Vec<&str>.
Email-Adress-Splitter
pub fn email_domain(email: &str) -> Option<&str> {
let (_user, domain) = email.split_once('@')?;
if domain.is_empty() || !domain.contains('.') {
return None;
}
Some(domain)
}
fn main() {
assert_eq!(email_domain("user@example.com"), Some("example.com"));
assert_eq!(email_domain("ohne_at"), None);
assert_eq!(email_domain("at@nodot"), None);
}Klassisches String-Parsing-Beispiel — split_once plus Validierung, alles auf Slices.
Interessantes
&str ist der idiomatische String-Parameter.
Funktioniert mit Literalen, mit &String (Deref-Coercion), mit Sub-Slices. &String als Parameter schließt Literale aus und ist fast immer schlechter. Clippy warnt mit clippy::ptr_arg.
&str garantiert UTF-8.
Jeder &str ist garantiert wohlgeformter UTF-8. Methoden wie chars() und lines() dürfen das annehmen. Wer rohe Bytes braucht: as_bytes() gibt &[u8].
String-Literale haben 'static-Lifetime.
"Hallo" ist &'static str. Die Bytes liegen im Read-Only-Datensegment des Binaries — kein Heap, keine Allocation, lebt für die gesamte Programm-Laufzeit.
len() ist Bytes, chars().count() ist Zeichen.
Klassische Falle. "Müller".len() ist 7, "Müller".chars().count() ist 6. Bei ASCII identisch, bei jedem Multi-Byte-Zeichen unterschiedlich. Für UI-Anzeige meist chars().count() nutzen.
Slicing in der Mitte eines Multi-Byte-Zeichens panickt.
&"Müller"[..2] panickt zur Laufzeit. Lösung: is_char_boundary vorher prüfen, oder Methoden wie floor_char_boundary (seit Rust 1.79) nutzen.
split_once ist oft besser als splitn(2).
split_once(',') gibt direkt Option<(&str, &str)> zurück — ideal für „Header: Wert"- und „Key=Value"-Patterns. Weniger Boilerplate als splitn(2, ...).collect::<Vec<_>>() plus Index-Zugriff.
strip_prefix / strip_suffix sind sicher gegen partielle Übereinstimmungen.
Wer „mache aus 'log.txt' das 'log'" will, schreibt s.strip_suffix(".txt") statt &s[..s.len()-4]. Erste Variante ist robust gegen Strings, die kein .txt-Suffix haben.
Methoden wie to_uppercase allokieren.
Jede Transformation, die einen neuen String produziert (to_uppercase, replace, repeat), macht eine Heap-Allocation. In Hot-Paths besser Iteratoren über chars() und direkte Buffer-Manipulation.
Weiterführende Ressourcen
Externe Quellen
- std::primitive.str
- The Rust Book – Slice Type
- The Rust Book – Storing UTF-8
- Clippy – ptr_arg
- Rust API Guidelines – Naming