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.

Rust &str
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 &str nicht mutieren.
  • Variable Lebensdauer (Lifetime). String-Literale haben 'static — sie leben für die gesamte Programm-Laufzeit. &str aus einem String lebt nur, solange der String lebt.
  • Fat Pointer im Speicher: Datenzeiger + Länge in Bytes.
  • Copy in 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:

Rust String
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 bei Vec<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).
Rust Idiomatische Signaturen
// ✓ 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:

Rust Konvertierungen
// 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, was Display implementiert (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:

Rust Größen
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 jedem push.

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.

Rust Mutation
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ücksetzen
Rust Lesen (auch auf &str)
let 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:

Rust Literale
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:

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

Rust Request-Builder
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

Rust Log-Helper
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

Rust DB-Row zu Struct
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

Rust Read-only Config
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

/ Weiter

Zurück zu Strings & Text

Zur Übersicht