Api-Design

192. impl Into<String> — Take Owned or Borrowed Without an Extra Allocation

Bite 191 said: if you only read the argument, take &str. But what if you need to store it? Taking &str and calling .to_owned() always allocates — even when the caller handed you a String it was about to throw away. impl Into<String> fixes that.

The hidden re-allocation

When a function keeps the value, the “take &str” rule turns into a trap:

1
2
3
4
5
struct Label { text: String }

fn make_label(text: &str) -> Label {
    Label { text: text.to_owned() } // always allocates
}

A literal caller has to allocate eventually — fair enough. But look what happens when the caller already owns a String:

1
2
3
4
let owned = String::from("Status: OK");
let label = make_label(&owned);
// `owned` is copied into a brand-new allocation, then `owned` is dropped.
// We threw away a perfectly good String and allocated a second one.

The caller had an owned buffer it no longer needed, and we ignored it.

Accept anything that becomes a String

Take impl Into<String>. A String moves in with zero copying; a &str allocates exactly once — never more:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Label { text: String }

fn make_label(text: impl Into<String>) -> Label {
    Label { text: text.into() }
}

// Literal: one allocation, unavoidable since we store it.
let a = make_label("Status: OK");

// Owned String: MOVED in. No copy, no second allocation.
let owned = String::from("Status: OK");
let b = make_label(owned);

assert_eq!(a.text, "Status: OK");
assert_eq!(b.text, "Status: OK");

Same call site for both, and the owned case is now free. The conversion happens lazily at the boundary, exactly once, and only when it must.

When you only read: impl AsRef

If you don’t store the value but still want to accept more than deref coercion allows (String, &str, Box<str>, Cow<str>, …), reach for impl AsRef<str>:

1
2
3
4
5
6
7
fn shout(text: impl AsRef<str>) -> String {
    text.as_ref().to_uppercase()
}

assert_eq!(shout("hi"), "HI");                       // &str
assert_eq!(shout(String::from("hi")), "HI");          // String
assert_eq!(shout(Box::<str>::from("hi")), "HI");      // Box<str>

as_ref() is a cheap borrow — no allocation — and the generic accepts every string-like type without forcing the caller to convert first.

The rule of thumb

If the function stores the string, take impl Into<String> so an owned argument moves in for free. If it only reads but you want maximum flexibility, take impl AsRef<str>. Plain &str (bite 191) is still the right default for simple read-only functions — these two just cover the cases it can’t.

191. Accept &str, Not String — Take the Most General Borrow

A function that takes String forces every caller holding a &str to allocate just to call you. Take &str instead — and &[T] over &Vec<T> — and deref coercion lets everyone in for free.

The over-specific signature

This function only ever reads its argument, yet it demands an owned String:

1
2
3
fn greeting(name: String) -> String {
    format!("Hello, {name}!")
}

Now a caller with a string literal — the most common case — has to allocate a whole String just to satisfy the type:

1
let g = greeting("Ferris".to_string()); // pointless allocation

Worse, a caller who only has a borrow (say, a field of someone else’s struct) is stuck: they must .clone() or .to_owned() before they can call you, even though you never keep the value.

Take the borrow

If the body only reads, take &str. Deref coercion means &String and string literals both coerce to &str automatically:

1
2
3
4
5
6
7
fn greeting(name: &str) -> String {
    format!("Hello, {name}!")
}

let owned = String::from("Ferris");
assert_eq!(greeting("Ferris"), "Hello, Ferris!"); // literal, no alloc
assert_eq!(greeting(&owned), "Hello, Ferris!");    // &String coerces to &str

Zero allocations at the call site, and every kind of caller just works.

Same rule for slices

The exact parallel exists for vectors. Taking &Vec<T> locks callers into owning a Vec; taking &[T] accepts a Vec, an array, or any slice:

1
2
3
4
5
6
7
8
9
fn total(nums: &[i32]) -> i32 {
    nums.iter().sum()
}

let v = vec![1, 2, 3];
let a = [4, 5, 6];
assert_eq!(total(&v), 6);       // &Vec<i32> coerces to &[i32]
assert_eq!(total(&a), 15);      // array coerces too
assert_eq!(total(&v[1..]), 5);  // a sub-slice — impossible with &Vec

&Vec<T> couldn’t accept that array or that sub-slice at all. &[T] is strictly more flexible and costs nothing.

The general principle

Borrow the least specific type that still does the job: &str over &String, &[T] over &Vec<T>, &Path over &PathBuf. Owned types in arguments are for when the function actually needs to store the value. If it only reads, hand it the borrow — the caller keeps their allocation, and your function works with more types for free.