&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:

Rust Speicher-Layout
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)

Rust Literal
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

Rust Borrow
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-Index

Drei syntaktische Varianten, die alle dasselbe produzieren — eine &str-Referenz, die auf denselben Heap-Buffer wie der ursprüngliche String zeigt. &owned ist die kürzeste und idiomatischste, weil sie Auto-Deref-Coercion nutzt; as_str() ist die explizite Form, die manchmal lesbarer ist, wenn der Code-Reviewer auf den Wechsel der Typ-Sicht aufmerksam werden soll; &owned[..] ist die Range-Index-Form, die zwar funktioniert, aber unnötig umständlich wirkt.

In allen drei Fällen passiert dasselbe: ein Fat Pointer wird auf den Stack gelegt, der auf die ersten Bytes des String-Heap-Allocates zeigt. Keine Kopie, keine Allocation — nur ein neuer 16-Byte-Header.

3. Sub-Slice eines anderen &str

Rust Sub-Slice
fn main() {
    let s = "Hallo, Welt!";
    let begruessung: &str = &s[..5];    // "Hallo"
    let komma: &str = &s[5..6];          // ","
    let suffix: &str = &s[7..];          // "Welt!"
}

Sub-Slicing ist eine der zentralen Operationen für String-Verarbeitung. Du nimmst einen größeren &str und erzeugst einen kleineren, der auf einen Teilbereich zeigt. Im Speicher ändert sich nichts — der neue Slice hat einen leicht verschobenen Pointer und eine kleinere Länge, aber zeigt in denselben Heap- oder Static-Bereich.

Wichtig: diese Operation ist nur sicher, wenn die Index-Grenzen an char-Grenzen liegen. Bei reinen ASCII-Strings (alle Zeichen 1 Byte) ist das immer der Fall; bei Mehrbyte-Strings wie "Müller" musst du vorsichtig sein — Slicing mitten in einem ü würde panicken.

UTF-8-Grenzen beachten

Rust Grenze
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

Rust Basics
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");                 // true

Iteration

Rust Iteration
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

Rust 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

Rust Find
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

Rust Trim
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)

Rust Transform
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);

Diese Transformations-Methoden produzieren immer einen neuen String — sie können nicht in-place arbeiten, weil &str immutable ist und weil die Output-Länge sich von der Input-Länge unterscheiden kann (etwa bei to_uppercase von Sonderzeichen). Jeder Aufruf alloziert einen Heap-Block für das Ergebnis.

In Hot-Paths, wo viele solche Transformationen pro Sekunde laufen, ist das ein spürbarer Overhead. Die Alternative: einen String-Buffer einmal vorab allokieren, in einer Schleife wiederverwenden, und die Transformationen über chars()/bytes() direkt hineinschreiben. Bei seltenen Aufrufen ist die einfache Variante mit allokierender Rückgabe aber meist die richtige Wahl — lesbarer und nur minimal langsamer.

Parsing

Rust parse
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. &str zurückzugeben funktioniert nur mit Lifetime-Verknüpfung zum Input.
Rust Idiomatisches API-Design
// ✅ 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:

Rust Raw-Strings
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:

Rust Lifetime-Varianten
// '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

Rust Validation
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");
}

Beide Funktionen sind pure &str-APIs — sie lesen den Input, prüfen oder schneiden ihn, und geben entweder einen booleschen Wert oder eine Sub-Slice-Referenz zurück. ohne_protokoll zeigt eine schöne Komposition: strip_prefix("https://") versucht zuerst den HTTPS-Strip, or_else greift, wenn das nicht geht, und versucht HTTP, schließlich gibt unwrap_or(url) den Original-String zurück, wenn beide Strip-Versuche scheitern.

Die Strip-Operationen sind sicherer als manuelles Slicing mit &url[7..] oder &url[8..] — sie panicken nicht, wenn das Präfix nicht passt, und sie sind robust gegen Sonderfälle wie leere Strings.

Header-Parsing

Rust HTTP-Header
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}");
    }
}

HTTP-Header-Parsing ist ein typischer String-Slice-Anwendungsfall. split_once(':') zerlegt die Zeile am ersten Doppelpunkt — wenn keiner gefunden wird, propagiert ? direkt None aus der Funktion. trim() entfernt Leerzeichen, ohne neue Strings zu allokieren — der Trim-Bereich wird nur durch Pointer-Verschiebung und Längen-Reduktion erreicht.

Das Ergebnis ist eine Option<(&str, &str)>: zwei Slice-Referenzen in den ursprünglichen Input-String. Bei tausenden Headern pro Request entstehen damit null Allocations für die Parser-Logik — die Daten leben weiter im ursprünglichen Buffer.

CSV-Zeile parsen

Rust CSV-Parser
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"]);
}

Eine CSV-Zeile in Felder zu zerlegen ohne eine einzige String-Allocation: split(',') produziert einen Iterator über &str-Sub-Slices, str::trim (als Method-Pointer benutzt) entfernt Leerzeichen pro Feld, collect::<Vec<&str>> sammelt die Slices in einen Vec. Der Vec besitzt nur die 16-Byte-Header der Slices; die eigentlichen Bytes leben weiter im Original-Input.

Bei großen CSV-Dateien ist das ein massiver Performance-Vorteil. Eine naive Variante mit String::from(...) pro Feld würde tausende Allocations machen; die Zero-Copy-Variante macht nur die eine für den Result-Vec. Der Trade-off: der Vec lebt nur so lange wie der Input — wenn du die Felder länger behalten willst, brauchst du .to_string()-Klone.

Multi-Line-Parser

Rust Config-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, mit Kommentar- und Leerzeilen-Filter, ohne eine einzige Allocation der Strings. Die Iterator-Pipeline läuft auf &str-Slices: lines() zerlegt in Zeilen, filter entfernt leere und Kommentar-Zeilen, filter_map(split_once) produziert nur Zeilen mit =-Zeichen, map(trim) entfernt Whitespace.

Das Ergebnis ist ein Vec<(&str, &str)> — die Tupel halten Slice-Referenzen in den Input-Text. Damit ist die Funktion nicht nur einfach zu lesen, sondern auch extrem effizient. Bei einer Config-Datei mit hundert Einträgen entstehen genau zwei Allocations: eine für den Result-Vec, eine optional für die cfg-String-Bindung im Aufrufer.

URL-Pfad und Query-String trennen

Rust URL-Split
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"));
}

URL-Parsing in seiner einfachsten Form: trenne den Pfad-Teil vom Query-String-Teil. find('?') durchsucht den String byte-weise nach dem ersten ? und gibt die Byte-Position zurück. Da ? ein 1-Byte-ASCII-Zeichen ist, ist die Position immer eine gültige UTF-8-Char-Grenze — Slicing davor und danach ist sicher.

Bei komplexerem URL-Parsing (mit URL-Encoding, Fragments, Path-Komponenten) würdest du eine Crate wie url nutzen. Aber für einfache Cases ist diese Inline-Variante ausreichend und schnell — null Allocations, direkte Slice-Operations.

Log-Level-Filter

Rust Log-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())
}

Log-Verarbeitung ist eines der häufigsten Anwendungsgebiete für &str-Pipelines. ist_fehler_zeile prüft, ob eine Log-Zeile mit einem von mehreren möglichen Präfixen beginnt — die Iterator-Form mit .iter().any(...) ist eleganter als drei separate if-Verzweigungen.

extrahiere_message schneidet den Header-Teil ab und liefert nur die eigentliche Message. Der Ablauf: find(']') sucht das schließende Bracket der Level-Klammer, map(|pos| ...) produziert den Sub-Slice ab der Position danach (mit trim_start für sauberes Whitespace). Wenn kein ] gefunden wird, gibt find None, und map produziert None — die Funktion verhält sich robust.

Word-Count

Rust Word-Counter
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 — bei einem Wort wie "der", das hundertmal vorkommt, gibt es nur eine Slice-Referenz im Set, nicht hundert String-Allocations. Das spart bei großen Texten massiv Speicher.

Der Trade-off ist die Lifetime-Bindung: die Map lebt nur so lange wie der Input-Text. Wenn du das Resultat länger behalten willst, müsstest du String-Klone nutzen (text.split_whitespace().map(String::from).fold(...)). Bei kurzlebigen Analysen ist die Borrow-Variante meist die richtige Wahl.

Tag-Renderer

Rust Tag-Print
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");
}

Hier ist eine kleine Kombination zweier Slice-Konzepte: &[&str] ist ein Slice von String-Slices. Der äußere Slice gibt eine Sequenz vor, jedes Element davon ist selbst ein &str-Slice. Damit kannst du die Funktion mit Array-Literalen aufrufen (["a", "b", "c"]), mit Vec<&str>, mit Vec<String> (durch Deref-Kette), oder mit einem Sub-Bereich davon.

Die Implementation zeigt eine typische Collect-Join-Pipeline: iter über die Slices, map zum Formatieren (das #-Präfix), collect::<Vec<_>> zum Materialisieren der einzelnen Hash-Tags, dann join(" ") zum Konkatenieren mit Leerzeichen.

Email-Adress-Splitter

Rust Email-Domain
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);
}

Email-Validierung in seiner einfachsten Form. split_once('@') zerlegt den String am ersten @-Zeichen — wenn keines vorhanden ist, gibt es None, was per ? aus der Funktion propagiert. Der Domain-Teil wird dann zwei zusätzlichen Checks unterzogen: nicht leer, und mindestens ein Punkt (für TLD).

Echte Email-Validierung ist deutlich komplexer (RFC 5322 hat viele Sonderfälle), aber für den 90 %-Anwendungsfall reicht diese Variante. Wieder zero-copy: das Ergebnis ist ein Slice in den Input, keine Allocations.

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

/ Weiter

Zurück zu Slices & Views

Zur Übersicht