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:
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 ownedStringmit Heap-Allokation.
Cow ist ein Enum mit zwei Varianten. Bei der Verwendung wirkt es aber wie ein &str, weil es Deref<Target=str> implementiert.
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:
// 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:
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
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):
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:
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:
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:
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.
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("<"),
'>' => out.push_str(">"),
'&' => out.push_str("&"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => 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:
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:
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:
# // 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
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
- std::borrow::Cow
- std::borrow::ToOwned
- The Rust Book – Smart Pointers
- serde – Lifetime borrowing
- Rust by Example – Cow