Cow for API Flexibility
Cow for API Flexibility
Level 18 — Rust Using
Cow<'_, str>orCow<'_, [T]>in APIs to accept both owned and borrowed data, cloning only when mutation is needed.
1. Prerequisites
Cow<'a, T>— Cow smart pointer.
2. Term Category
Rust Idiom Pattern (clone-on-write zero-copy flexibility): std::borrow::Cow (Copy-On-Write) for opportunistic borrowing.
3. Explanation
(1) Design Motivation — "Why did we design this?"
Allocating memory on the heap for string transformations (like sanitizing HTML or unescaping strings) is expensive if 95% of inputs require no modification.
Cow (Copy-On-Write) is an enum (Borrowed(&'a B) vs Owned(B::Owned)) that allows borrowing data immutably zero-cost, allocating dynamic heap memory only when mutation occurs.
(2) Reality Metaphor
A document editor previewing a manuscript: you read the original file directly from disk (Borrowed). The moment you type a single edit, the system creates a private working copy (Owned).
(3) Rust Code Examples
Short Snippet
use std::borrow::Cow;
fn sanitize(s: &str) -> Cow<str> {
if s.contains('<') { Cow::Owned(s.replace('<', "<")) }
else { Cow::Borrowed(s) }
}
Fuller Example
use std::borrow::Cow;
fn escape_html(input: &str) -> Cow<str> {
if input.contains('<') || input.contains('>') {
let mut s = String::with_capacity(input.len());
for c in input.chars() {
match c {
'<' => s.push_str("<"),
'>' => s.push_str(">"),
_ => s.push(c),
}
}
Cow::Owned(s)
} else {
Cow::Borrowed(input)
}
}
fn main() {
let clean = "hello_world";
let res1 = escape_html(clean);
assert!(matches!(res1, Cow::Borrowed(_)));
}
4. Common Mistakes & Pitfalls
Mistake 1: Calling .to_mut() on Cow Unnecessarily
The mistake: Calling .to_mut() on a borrowed Cow when no modification is made.
Why it is wrong: Calling .to_mut() immediately clones the borrowed data into owned heap memory even if no edits are performed.
Incorrect:
let mut c = Cow::Borrowed("data"); c.to_mut(); // Clones data!
Fix:
let mut c = Cow::Borrowed("data"); if needs_edit { c.to_mut().push_str("!"); }
Mistake 2: Forgetting Cow Implements Deref
The mistake: Writing explicit match blocks just to read Cow values.
Why it is wrong: Cow implements Deref, allowing transparent read access to methods on the underlying target (str or [T]).
Incorrect:
match cow { Cow::Borrowed(s) => s.len(), Cow::Owned(ref s) => s.len() }
Fix:
cow.len() // Automatic Deref coercion!
Mistake 3: Using Cow for Always-Mutated Strings
The mistake: Using Cow<str> when function inputs are guaranteed to be transformed 100% of the time.
Why it is wrong: Introduces enum matching overhead without saving any allocations.
Incorrect:
fn always_transform(s: &str) -> Cow<str> { Cow::Owned(s.to_uppercase()) }
Fix:
fn always_transform(s: &str) -> String { s.to_uppercase() }
5. Practice Exercises
Exercise 1: Zero-Copy HTTP Header Normalizer
Scenario: Build an HTTP header value normalizer that strips leading whitespace using Cow<str>.
Requirements:
- Implement
normalize_header(val: &str) -> Cow<str>. - Return
Cow::Borrowedif already trimmed. - Return
Cow::Ownedif trimming occurs. - Include unit tests for borrowed vs owned variants.
Answer
Implementation
use std::borrow::Cow;
pub fn normalize_header(val: &str) -> Cow<str> {
let trimmed = val.trim();
if trimmed.len() == val.len() {
Cow::Borrowed(val)
} else {
Cow::Owned(trimmed.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_borrowed() {
let clean = "application/json";
let res = normalize_header(clean);
assert!(matches!(res, Cow::Borrowed(_)));
assert_eq!(res, "application/json");
}
#[test]
fn test_header_owned() {
let dirty = " text/html ";
let res = normalize_header(dirty);
assert!(matches!(res, Cow::Owned(_)));
assert_eq!(res, "text/html");
}
}
Technical Explanation
normalize_headerchecks string length after trimming.- If no whitespace exists, it returns zero-allocation
Cow::Borrowed(val). - Allocates
Stringheap memory only when trimming occurs.
Exercise 2: Zero-Copy SQL Identifier Escaper
Scenario: Implement a SQL column name escaper wrapping invalid characters in quotes.
Requirements:
- Escape spaces in identifiers.
- Return
Cow<str>.
Answer
Implementation
use std::borrow::Cow;
pub fn escape_sql_identifier(id: &str) -> Cow<str> {
if id.contains(' ') || id.contains('-') {
Cow::Owned(format!("\"{}\"", id.replace('"', """")))
} else {
Cow::Borrowed(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sql_escaper() {
let valid = "user_id";
let invalid = "user name";
assert!(matches!(escape_sql_identifier(valid), Cow::Borrowed(_)));
assert!(matches!(escape_sql_identifier(invalid), Cow::Owned(_)));
}
}
Technical Explanation
- Avoids allocating heap memory for standard valid database identifier column names.
- Allocates only when escaping spaces/hyphens.
Exercise 3: Opportunistic In-Place Array Mutation
Scenario: Implement a Copy-on-Write buffer Cow<[i32]> replacing negative numbers with zeros.
Requirements:
- Accept
Cow<[i32]>. - Replace negatives zero-copy where possible.
Answer
Implementation
use std::borrow::Cow;
pub fn sanitize_signal(data: &[i32]) -> Cow<[i32]> {
if let Some(idx) = data.iter().position(|&x| x < 0) {
let mut owned = data.to_vec();
for val in &mut owned[idx..] {
if *val < 0 {
*val = 0;
}
}
Cow::Owned(owned)
} else {
Cow::Borrowed(data)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signal_sanitizer() {
let clean = [1, 2, 3];
let dirty = [1, -5, 3];
assert!(matches!(sanitize_signal(&clean), Cow::Borrowed(_)));
assert_eq!(sanitize_signal(&dirty)[1], 0);
}
}
Technical Explanation
- Operates on slice
&[i32]zero-copy if all signals are non-negative. - Clones into
Vec<i32>only when a negative sample is encountered.
6. Related Terms
Cow<'a, T>— Clone-on-write pointer.ToOwnedTrait — ToOwned trait.
7. Key Takeaways
- Provides Copy-On-Write opportunistic borrowing (
Cow::BorrowedvsCow::Owned). - Reduces heap allocations in read-heavy string and slice workflows.
- Implements
Dereffor transparent access to underlying type methods. - Use
.to_mut()only when mutation is actually performed.