Slicing in Rust ist eine zweischneidige Sache: für ASCII-Strings funktioniert &s[0..5] intuitiv und schnell, für UTF-8-Strings mit Multi-Byte-Zeichen kann derselbe Code zu einem Panic führen. Der Grund liegt im variable-length encoding: ein ü belegt 2 Bytes, ein 🦀 4 Bytes — und ein Slice-Index darf nie mitten in einem Zeichen landen. Dieser Artikel erklärt, was bei &s[i..j] wirklich passiert, wie du sicher slicest und wann der Wechsel zu as_bytes() der richtige Weg ist.

Slicing-Grundlagen

Slicing auf str und String funktioniert mit Byte-Indices, nicht Zeichen-Indices:

Rust ASCII — alles geht
fn main() {
    let s = "Hallo, Welt!";
    let a: &str = &s[..5];     // "Hallo"
    let b: &str = &s[7..];      // "Welt!"
    let c: &str = &s[..];       // "Hallo, Welt!"
    println!("{a} {b}");
}

Bei reinem ASCII ist jeder Byte-Index gleichzeitig ein Zeichen-Index. Solange du nur ASCII verarbeitest, ist &s[i..j] unproblematisch.

UTF-8 und Multi-Byte-Zeichen

Sobald Nicht-ASCII ins Spiel kommt, kann ein naiver Index ein Problem werden:

Rust Multi-Byte-Falle
fn main() {
    let s = "Müller";
    println!("Länge in Bytes: {}", s.len());           // 7 (nicht 6!)
    println!("Anzahl Zeichen: {}", s.chars().count()); // 6

    let h: &str = &s[..1];      // "M" — ok, M ist 1 Byte
    // let bad: &str = &s[..2];  // Panic! — Grenze mitten in 'ü'
    let ok: &str = &s[..3];     // "Mü" — ok, ü endet bei Byte 3
}

Der String "Müller" belegt 7 Bytes:

Byte-IndexWertZeichen
00x4DM
10xC3ü (Byte 1)
20xBCü (Byte 2)
30x6Cl
40x6Cl
50x65e
60x72r

&s[..2] würde nur das erste Byte von ü mitnehmen — kein gültiges UTF-8. Rust verhindert das mit einem Laufzeit-Panic.

Die Panic-Meldung

Rust
thread 'main' panicked at 'byte index 2 is not a char boundary;
it is inside 'ü' (bytes 1..3) of `Müller`'

Hilfreich: die Panic-Meldung zeigt exakt, welches Zeichen an der Grenze sitzt und in welchem Byte-Bereich es liegt.

Sicheres Slicing

Drei Werkzeuge, um Slicing-Panics zu vermeiden:

1. is_char_boundary — vorher prüfen

Rust char-Grenze prüfen
fn sicher_slicen(s: &str, n: usize) -> &str {
    if s.is_char_boundary(n) {
        &s[..n]
    } else {
        ""
    }
}

is_char_boundary(i) gibt true zurück, wenn i entweder 0, s.len() oder an einer Zeichen-Grenze in UTF-8 liegt.

2. get(...) — Option zurück statt Panic

Rust get statt Index
let s = "Müller";
let teil: Option<&str> = s.get(0..2);    // None — keine gültige Grenze
let teil: Option<&str> = s.get(0..3);    // Some("Mü")

s.get(range) ist die fallible Variante von &s[range]. Empfehlung: wenn dein Code mit fremdem Input arbeitet, get statt direktem Slicing.

3. floor_char_boundary / ceil_char_boundary

Seit Rust 1.79 gibt es Methoden, die zur nächsten gültigen Grenze runden:

Rust Auf gültige Grenze runden
let s = "Müller";
let runter: usize = s.floor_char_boundary(2);    // 1 (eine Grenze davor)
let hoch: usize = s.ceil_char_boundary(2);       // 3 (eine Grenze danach)
let teil = &s[..runter];                          // "M"

Ideal für „kürze einen String auf höchstens N Bytes, aber an einer gültigen Zeichen-Grenze".

Byte-Zugriff mit as_bytes

Wenn du wirklich byteweise arbeiten willst (z. B. bei Netzwerk-Protokollen oder Binary-Pattern-Suche), gibt es as_bytes():

Rust Byte-View
let s = "Müller";
let bytes: &[u8] = s.as_bytes();
println!("{:?}", bytes);
// [77, 195, 188, 108, 108, 101, 114]

for &b in bytes {
    print!("{b:02x} ");
}
// 4d c3 bc 6c 6c 65 72

as_bytes() ist kostenlos — keine Allokation, kein Copy. Es ist nur eine andere Sicht auf dieselben Speicher-Bytes.

Zurück: from_utf8

Wenn du einen &[u8] hast und ihn als &str interpretieren willst:

Rust Bytes zu &str
let bytes = b"Hallo";        // &[u8; 5]

// Validierende Konvertierung
let s: Result<&str, _> = std::str::from_utf8(bytes);
match s {
    Ok(text) => println!("{text}"),
    Err(e) => eprintln!("Kein UTF-8: {e}"),
}

// Unsicher (überspringt Validierung)
let unsafe_s: &str = unsafe { std::str::from_utf8_unchecked(bytes) };

Die sichere Variante prüft, ob die Bytes wohlgeformt UTF-8 sind, und gibt sonst einen Utf8Error. Die unchecked-Variante überspringt die Prüfung — schneller, aber unsafe, weil invalides UTF-8 in einem &str zu Undefined Behavior führt.

Byte-Literale und Byte-Strings

Rust hat eigene Literal-Syntaxen für rohe Bytes:

Rust Byte-Literale
let einzel: u8 = b'A';                    // 65
let array: &[u8; 5] = b"Hallo";           // [72, 97, 108, 108, 111]
let raw: &[u8; 4] = b"\xDE\xAD\xBE\xEF";  // binäre Bytes

// Bytestring kann nur ASCII (oder Hex-Escapes) enthalten
// let umlaut = b"Müller";  // Fehler — kein UTF-8 in Byte-Literalen

Byte-Strings (b"...") sind &[u8; N], nicht &str. Sie sind ideal für Binär-Protokolle oder Datei-Headers.

find, position, byte-vs-char

Suche in Strings liefert immer Byte-Indices, nicht Zeichen-Indices:

Rust Byte-Position
let s = "Café";
let pos = s.find('f');             // Some(2) — Byte-Index nach 'C' und 'a'
let pos2 = s.find('é');             // Some(3) — Byte-Index nach 'Caf'

// Mit Slicing zusammen — funktioniert immer korrekt,
// weil find() garantiert eine gültige char-Grenze liefert
if let Some(p) = s.find('f') {
    println!("Davor: {}", &s[..p]);    // "Ca"
    println!("Ab dort: {}", &s[p..]);   // "fé"
}

Wichtige Garantie: find, rfind, splitn und alle Suchmethoden liefern garantiert gültige Byte-Grenzen. Du kannst ihr Ergebnis sicher zum Slicing nutzen.

Praxis: Slicing in echtem Code

URL-Pfad und -Query trennen

Eine URL wie /users/42?include=email&fields=name in Pfad und Query-String aufteilen:

Rust URL-Split
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&fields=name";
    let (pfad, query) = split_pfad_query(url);
    assert_eq!(pfad, "/users/42");
    assert_eq!(query, Some("include=email&fields=name"));
}

find('?') liefert garantiert eine korrekte Byte-Grenze — also ist das Slicing absolut sicher, auch wenn der Rest UTF-8 mit Umlauten enthält.

Sicheres Truncate auf max. N Bytes

In Log-Output oder DB-Spalten brauchst du oft „kürze einen Text auf höchstens 200 Bytes, ohne ein Zeichen zu zerschneiden":

Rust Smart-Truncate
fn truncate_an_grenze(s: &str, max_bytes: usize) -> &str {
    if s.len() <= max_bytes {
        return s;
    }
    let mut i = max_bytes;
    while !s.is_char_boundary(i) {
        i -= 1;
    }
    &s[..i]
}

fn main() {
    let lang = "Ich bin ein längerer Text mit Umlauten äöü";
    let kurz = truncate_an_grenze(lang, 12);
    println!("{kurz}");          // Garantiert valides UTF-8
}

Auf modernen Rust (≥ 1.79) eleganter mit floor_char_boundary(max_bytes) als Einzeiler.

HTTP-Header parsen

Ein Header wie Content-Type: text/html; charset=utf-8 in Name, Wert, Parameter zerlegen:

Rust Header-Split
fn parse_header(zeile: &str) -> Option<(&str, &str)> {
    let pos = zeile.find(':')?;
    let name = zeile[..pos].trim();
    let wert = zeile[pos + 1..].trim();
    Some((name, wert))
}

fn main() {
    let h = "Content-Type: text/html; charset=utf-8";
    if let Some((name, wert)) = parse_header(h) {
        assert_eq!(name, "Content-Type");
        assert_eq!(wert, "text/html; charset=utf-8");
    }
}

Alle Operationen sind Slice-basiert — kein einziger neuer String wird alloziert. Das macht Header-Parsing in Rust extrem schnell.

Magic-Bytes prüfen

Dateiformate haben oft Magic-Bytes am Anfang. PNG beginnt mit \x89PNG\r\n\x1a\n, JPEG mit \xFF\xD8\xFF:

Rust Datei-Format erkennen
fn erkenne_format(daten: &[u8]) -> &'static str {
    if daten.starts_with(b"\x89PNG\r\n\x1a\n") { return "PNG"; }
    if daten.starts_with(&[0xFF, 0xD8, 0xFF])  { return "JPEG"; }
    if daten.starts_with(b"GIF87a") || daten.starts_with(b"GIF89a") { return "GIF"; }
    "unbekannt"
}

Hier ist Byte-View (&[u8]) genau richtig — kein UTF-8 nötig. Die binären Magic-Bytes interessieren als rohe Bytes, nicht als Text.

Häufige Stolperfallen

s.len() ist die Byte-Länge, nicht die Zeichen-Anzahl.

Der häufigste Fallstrick. "Café".len() ist 5 (vier Bytes für „Caf" plus 2 für „é"... eigentlich 5). "Café".chars().count() ist 4. Wer eine UI-Wortzählung oder ein Zeichen-Limit braucht, immer chars().count() nutzen.

&s[i..j] kann zur Laufzeit panicken.

Bei Multi-Byte-Inhalten ist das ein realistisches Risiko. In produktivem Code mit user-input besser s.get(i..j) (gibt Option) oder vorher is_char_boundary prüfen.

find liefert Byte-Indices — auch bei chars.

s.find('é') gibt die Byte-Position, nicht die char-Position. Das ist Absicht: die zurückgegebene Position ist sofort zum Slicing nutzbar (&s[..pos]) — eine char-Position müsste man erst in eine Byte-Position konvertieren.

Iteration über bytes() ist NICHT Iteration über chars().

"Müller".bytes().count() ist 7, "Müller".chars().count() ist 6. bytes() liefert u8-Werte (rohe UTF-8-Bytes), chars() dekodierte 4-Byte-Code-Points. Für Text-Analyse fast immer chars().

Slice-Grenzen von find/rfind/split sind garantiert valide.

Du musst dir um is_char_boundary keine Sorgen machen, wenn der Index aus einer Suchfunktion stammt. Die Stdlib gibt nur Byte-Positionen zurück, die wirklich Zeichen-Grenzen sind. Erst bei manuell konstruierten Indices wird's gefährlich.

Strings sind UTF-8, aber OsString/PathBuf nicht zwingend.

Dateinamen auf Linux dürfen beliebige Bytes (außer 0) sein, auf Windows UTF-16-Surrogates. Deshalb gibt es OsString und OsStr als Sondertypen — sie können nicht naiv in String konvertiert werden. Genauer im Standard-Library-Kapitel.

as_bytes() ist kostenlos, into_bytes() konsumiert den String.

s.as_bytes() gibt &[u8] — Borrow, kein Move. s.into_bytes() konsumiert den String und gibt Vec<u8> — der String ist danach weg. Beide sind ohne Allokation, weil der Speicher derselbe ist.

Byte-Literale sind &[u8; N], keine &str.

b"Hallo" hat Typ &[u8; 5]. Der wichtige Unterschied: keine UTF-8-Garantie, kein direkter String-Vergleich mit &str. Praktisch für Binär-Protokolle, irreführend, wenn man irrtümlich denkt, das wäre ein „byte-String" im JavaScript-Sinne.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Strings & Text

Zur Übersicht