&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, alle gleichbedeutend. &owned ist die idiomatischste — sie nutzt Auto-Deref-Coercion.

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!"
}

Range-Indexierung erzeugt einen neuen &str, der auf dieselben Bytes zeigt. Keine Allocation — nur ein neuer Pointer + Länge.

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

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

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

Pure &str-API — keine Allocation, kein Owned-Move. Aufrufer behält den Original-String.

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

split_once plus trim — beide arbeiten auf &str und geben &str zurück. Zero-Copy, zero-allocation.

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

Vec<&str> — der Vec besitzt die Slice-Pointer, die Daten leben weiter im Original-raw-String.

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 — ohne eine einzige Allocation der Strings selbst.

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

find('?') gibt eine garantiert gültige Byte-Grenze zurück — Slicing ist sicher.

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

Filter und Extraktion auf &str — typisch für Log-Verarbeitung in Pipelines.

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 — keine Heap-Allocation pro Wort. Lifetime der Map ist an Input gebunden.

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

Funktion nimmt einen &[&str] — Slice von String-Slices. Sehr flexibel: akzeptiert sowohl Arrays von Literalen als auch dynamisch gebaute Vec<&str>.

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);
}

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

/ Weiter

Zurück zu Slices & Views

Zur Übersicht