Cow<str> (gesprochen „Cow" wie das Tier, kurz für „Clone-on-Write") ist ein Smart-Pointer, der zwei Zustände kennt: entweder hält er eine borrowed &str-Referenz, oder einen owned String. Damit löst er ein häufiges Allokations-Dilemma: eine Funktion soll einen Text normalisieren, weiß aber nicht im Voraus, ob das Original schon sauber ist. Mit String allokiert sie immer, mit &str kann sie nicht modifizieren. Cow ist die elegante Mitte — alloziert nur dann, wenn wirklich modifiziert werden muss. Dieser Artikel zeigt das Konzept, die zwei Varianten, die Schnittstelle und die Patterns, mit denen Cow in idiomatischem Rust auftaucht.

Was Cow ist

Cow lebt in std::borrow::Cow und sieht so aus:

Rust Cow-Definition (vereinfacht)
enum Cow<'a, B: ?Sized + ToOwned> {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

Für Strings ist die typische Form Cow<'a, str> — und damit:

  • Cow::Borrowed(&'a str) — eine Referenz auf einen bestehenden String.
  • Cow::Owned(String) — ein owned String mit Heap-Allokation.

Cow ist ein Enum mit zwei Varianten. Bei der Verwendung wirkt es aber wie ein &str, weil es Deref<Target=str> implementiert.

Rust Verwendung
use std::borrow::Cow;

fn main() {
    let geliehen: Cow<str> = Cow::Borrowed("Hallo");
    let besessen: Cow<str> = Cow::Owned(String::from("Welt"));

    // Beide wirken wie &str
    println!("{}", geliehen.len());     // 5
    println!("{}", besessen.to_uppercase());

    // Auch direkt nutzbar
    for c in geliehen.chars() { print!("{c}"); }
}

Das Allokations-Dilemma

Stell dir vor, du schreibst eine Normalisierungs-Funktion: „entferne führende/folgende Leerzeichen und mache lowercase". Drei naive Signaturen:

Rust Drei Varianten
// Variante A: String hinein, String raus
fn normalisiere_a(s: String) -> String {
    s.trim().to_lowercase()
}
// Problem: alloziert IMMER, auch wenn Input schon normalisiert war.

// Variante B: &str hinein, String raus
fn normalisiere_b(s: &str) -> String {
    s.trim().to_lowercase()
}
// Problem: alloziert IMMER, auch wenn nichts zu tun ist.

// Variante C: &str hinein, &str raus
// fn normalisiere_c(s: &str) -> &str { ... }
// Geht nicht: zur Compile-Zeit ist unklar, ob wir den Original-Borrow
// zurückgeben können oder etwas Neues konstruieren müssen.

Variante D ist Cow:

Rust Variante D — Cow
use std::borrow::Cow;

fn normalisiere(s: &str) -> Cow<str> {
    let getrimmt = s.trim();
    let braucht_lowercase = getrimmt.chars().any(|c| c.is_uppercase());

    if getrimmt.len() == s.len() && !braucht_lowercase {
        Cow::Borrowed(s)        // ← keine Allokation
    } else {
        Cow::Owned(getrimmt.to_lowercase())
    }
}

fn main() {
    let a = normalisiere("hallo");          // Borrowed
    let b = normalisiere("  HALLO  ");      // Owned

    assert_eq!(&*a, "hallo");
    assert_eq!(&*b, "hallo");
    assert!(matches!(a, Cow::Borrowed(_)));
    assert!(matches!(b, Cow::Owned(_)));
}

Wenn der Eingabe-String schon sauber ist, gibt normalisiere eine Borrowed-Variante zurück — keine Heap-Allokation, kein Copy, der Aufrufer bekommt einen Pointer auf den Original-String. Erst wenn wirklich etwas geändert werden muss, fällt die Allokation an.

Konstruieren und konvertieren

Rust Konstruktion
use std::borrow::Cow;

// Aus &str (immer Borrowed)
let a: Cow<str> = Cow::Borrowed("hi");
let b: Cow<str> = "hi".into();             // auch Borrowed

// Aus String (immer Owned)
let c: Cow<str> = Cow::Owned(String::from("hi"));
let d: Cow<str> = String::from("hi").into();  // Owned

.into() ist sehr nützlich, weil es automatisch die richtige Variante wählt — &str wird zu Borrowed, String zu Owned.

into_owned und to_mut

Manchmal brauchst du eine garantiert owned Version (z. B. um zu mutieren):

Rust into_owned
use std::borrow::Cow;

fn modifiziere(cow: Cow<str>) -> String {
    let mut owned: String = cow.into_owned();    // Borrowed → String klont; Owned → bleibt
    owned.push_str(" - geändert");
    owned
}

fn main() {
    let a = modifiziere(Cow::Borrowed("hi"));
    assert_eq!(a, "hi - geändert");
}

to_mut ist die mutable-by-reference-Variante:

Rust to_mut
use std::borrow::Cow;

let mut cow: Cow<str> = Cow::Borrowed("hallo");
cow.to_mut().push_str(" welt");          // konvertiert intern zu Owned
assert_eq!(&*cow, "hallo welt");

to_mut gibt eine &mut String zurück. Wenn die Cow aktuell Borrowed ist, wird sie zuerst geklont und in Owned umgewandelt — danach kann man frei mutieren.

Cow als Funktions-Parameter

Cow funktioniert nicht nur als Rückgabewert, sondern auch als Parameter:

Rust Cow als Parameter
use std::borrow::Cow;

fn speichern(name: Cow<str>) {
    // Wir können den String hier mutieren oder nicht — flexibel
    println!("Speichere: {name}");
}

fn main() {
    speichern(Cow::Borrowed("hi"));            // Borrow ohne Alloc
    speichern("hi".into());                     // auch Borrow
    speichern(String::from("welt").into());     // Owned, einmaliger Move
    speichern(format!("{}-{}", "a", 1).into()); // Owned aus format!
}

In Praxis ist Cow-Parameter seltener als Cow-Return. Der häufigste Grund, ihn zu nehmen: die Funktion soll den Wert eventuell speichern und dafür ownership brauchen, aber der häufige Aufruf-Pfad ist mit einer Referenz zufrieden.

Cow für andere Typen

Cow ist generisch über B: ToOwned. Neben str gibt's auch:

Rust Andere Cow-Varianten
use std::borrow::Cow;
use std::path::Path;

// Cow<[T]> — Slice oder Vec
let a: Cow<[i32]> = Cow::Borrowed(&[1, 2, 3]);
let b: Cow<[i32]> = Cow::Owned(vec![1, 2, 3]);

// Cow<Path>
let p: Cow<Path> = Path::new("/tmp/x").into();

Das Pattern ist immer dasselbe: ein View oder ein owned Container, je nachdem, was zur Verfügung steht.

Praxis: Wo Cow im echten Code lebt

HTML-Escape — meistens kein Escape nötig

Bei HTML-Output muss jeder String mit <, >, &, " escapt werden. Aber die meisten Strings haben keine Sonderzeichen — der häufige Fall sollte ohne Allokation auskommen.

Rust HTML-Escape
use std::borrow::Cow;

fn html_escape(input: &str) -> Cow<str> {
    if !input.contains(['<', '>', '&', '"', '\'']) {
        return Cow::Borrowed(input);     // Schneller Pfad: kein Escape nötig
    }
    let mut out = String::with_capacity(input.len() + 16);
    for c in input.chars() {
        match c {
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '&' => out.push_str("&amp;"),
            '"' => out.push_str("&quot;"),
            '\'' => out.push_str("&#39;"),
            _ => out.push(c),
        }
    }
    Cow::Owned(out)
}

fn main() {
    let a = html_escape("Hallo Welt");                // Borrowed
    let b = html_escape("<script>alert(1)</script>");  // Owned
    assert!(matches!(a, Cow::Borrowed(_)));
    assert!(matches!(b, Cow::Owned(_)));
    println!("{}", b);
}

In einem Web-Framework, das tausende Strings rendert, spart das massive Allokationen — die meisten Inhalte (User-Namen, URLs, Zahlen als Strings) brauchen kein Escape.

Path-Normalisierung

Pfade auf Disk müssen oft normalisiert werden (/foo//bar/../baz/foo/baz). Wenn der Pfad schon normalisiert ist, kann der originale Borrow zurückgegeben werden:

Rust Pfad-Normalisierung
use std::borrow::Cow;

fn entferne_doppel_slashes(pfad: &str) -> Cow<str> {
    if !pfad.contains("//") {
        return Cow::Borrowed(pfad);
    }
    let mut out = String::with_capacity(pfad.len());
    let mut letzte_war_slash = false;
    for c in pfad.chars() {
        if c == '/' && letzte_war_slash {
            continue;
        }
        out.push(c);
        letzte_war_slash = c == '/';
    }
    Cow::Owned(out)
}

Übersetzung mit Fallback

Wenn eine Übersetzung gefunden wird, gibt es einen owned String (aus der Übersetzungs-Datenbank); wenn nicht, der originale Schlüssel:

Rust i18n-Lookup
use std::borrow::Cow;
use std::collections::HashMap;

struct I18n { strings: HashMap<String, String> }

impl I18n {
    fn uebersetze<'a>(&'a self, schluessel: &'a str) -> Cow<'a, str> {
        match self.strings.get(schluessel) {
            Some(uebersetzt) => Cow::Borrowed(uebersetzt.as_str()),
            None => Cow::Borrowed(schluessel),     // Fallback ohne Alloc
        }
    }
}

Beide Pfade sind Borrowed — der eine zeigt in die HashMap, der andere auf den Schlüssel selbst. Keine Allokation in beiden Fällen.

serde-Deserialisierung mit Cow

serde unterstützt Cow<str> direkt — wenn der JSON-Input keine Escape-Sequenzen hat, hält das Result einen Borrow auf den Original-Buffer:

Rust serde
# // Erfordert: serde = { version = "1", features = ["derive"] }
use std::borrow::Cow;
use serde::Deserialize;

#[derive(Deserialize)]
struct Event<'a> {
    #[serde(borrow)]
    typ: Cow<'a, str>,
    #[serde(borrow)]
    user: Cow<'a, str>,
}

#[serde(borrow)] weist den Deserializer an, wenn möglich, in den Input-String zu schauen. Bei JSON mit Escapes (z. B. "Mülleré") muss serde den String dekodieren und alloziert — dann wird Owned daraus.

Cache mit Cow-Werten

Rust Cache-Lookup
use std::borrow::Cow;
use std::collections::HashMap;

struct Cache { eintraege: HashMap<String, String> }

impl Cache {
    fn berechne_oder_hole<'a>(&'a self, schluessel: &str) -> Cow<'a, str> {
        if let Some(wert) = self.eintraege.get(schluessel) {
            Cow::Borrowed(wert.as_str())        // Cache-Hit: kein Alloc
        } else {
            Cow::Owned(format!("berechnet-{schluessel}"))   // Miss: neue Alloc
        }
    }
}

Cache-Hits sind in dieser Funktion ohne Alloc, Cache-Misses produzieren eine neue Berechnung als Owned. Die Signatur lässt beide Pfade transparent zu.

Besonderheiten

Cow ist ein Enum mit zwei Varianten, kein String mit Magie.

Intern: ein Tag-Byte plus entweder ein Pointer (für Borrowed) oder ein 24-Byte-String-Header (für Owned). Größe: 32 Bytes auf 64-bit. Etwas dicker als &str (16) oder String (24), dafür flexibler.

Deref macht Cow transparent.

Du musst nicht explizit match machen — cow.len(), cow.chars(), &cow[..3] funktionieren direkt. Hinter den Kulissen läuft das über Deref<Target=str>. Erst wenn du auf die owned-Variante zugreifen willst, brauchst du to_mut oder into_owned.

to_mut klont, into_owned verbraucht.

cow.to_mut() gibt &mut String — wenn nötig durch Klonen. Die Cow bleibt benutzbar. cow.into_owned() verbraucht die Cow und gibt einen String. Beide haben ihren Platz: to_mut wenn du mehrere Mutationen machst, into_owned wenn du nur am Ende einen owned brauchst.

Cow macht keine Performance-Magie out-of-thin-air.

Wenn dein Algorithmus 100 % der Aufrufe Mutation erfordert, ist Cow overhead — eine String-Variante wäre besser. Cow zahlt sich aus, wenn der häufige Fall keine Mutation braucht und nur Ausnahmen allokieren müssen.

Lifetime-Annotationen werden bei Cow häufiger sichtbar.

fn foo<'a>(s: &'a str) -> Cow<'a, str> macht klar: der Borrowed-Fall ist an die Lebensdauer von s gebunden. Bei String-Returns gibt es das Problem nicht. Wer mit Cow-Returns arbeitet, lernt Lifetime-Annotationen idiomatisch zu schreiben.

Cow ist nicht das gleiche wie Cow.

Cow<str> ist der idiomatische Typ — str ist das unsized DST, und Cow arbeitet als „Owned ist <str as ToOwned>::Owned = String". Cow<String> würde funktionieren, ist aber nicht idiomatisch und liefert nur Borrowed(&String) statt Borrowed(&str).

Klassisches Anwendungsgebiet: serde-Deserialisierung.

#[serde(borrow)] mit Cow<'_, str> ist das Standard-Pattern für „pari deserialization", bei der die meisten Strings direkt in den Input-Buffer zeigen — nur Strings mit Escape-Sequenzen werden alloziert. Riesiger Performance-Boost bei großen JSON-Streams.

Cow kann verschachtelt werden, aber selten sinnvoll.

Cow<'a, Cow<'b, str>> ist syntaktisch erlaubt, semantisch verwirrend. Wer drei Ebenen Cow ineinander hat, sollte das Design überdenken — meistens reicht eine Ebene.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Strings & Text

Zur Übersicht