S05: Owned parameters where a borrow is sufficient
What it looks like
Section titled “What it looks like”pub fn greeting(name: String) -> String { format!("Dear {name},")}
pub fn count_large(orders: Vec<Order>, min_cents: u64) -> usize { orders.iter().filter(|o| o.total_cents >= min_cents).count()}And at the call site:
let first = &orders[0];let text = greeting(first.customer.clone()); // copy only to call the functionlet large = count_large(orders.to_vec(), 10_000); // copy the whole listWhy AI agents produce it
Section titled “Why AI agents produce it”- Java habit. In Java,
String nameandList<Order> ordersare always references, so the parameter type does not say anything about ownership. The agent writes the Rust type that “looks the same”. - It avoids lifetime questions. Owned types never need a lifetime annotation, so an agent that had a lifetime error once may use them everywhere.
Why it is a problem
Section titled “Why it is a problem”- Callers must copy. A caller that still needs the value after the call must
.clone()it first (S01). - The signature says the wrong thing.
fn greeting(name: String)reads as “this function keeps or consumes the name”. It only formats it. - Less flexible.
&straccepts aString, a string literal, or part of a string.Stringaccepts only aString.
The better version
Section titled “The better version”WorseTakes ownership of data that it only reads.
pub fn greeting(name: String) -> String {format!("Dear {name},")}pub fn count_large(orders: Vec<Order>, min_cents: u64) -> usize {orders.iter().filter(|o| o.total_cents >= min_cents).count()}BetterBorrows a
&strand a&[Order]. Callers keep their values.pub fn greeting(name: &str) -> String {format!("Dear {name},")}pub fn count_large(orders: &[Order], min_cents: u64) -> usize {orders.iter().filter(|o| o.total_cents >= min_cents).count()}
The call site no longer copies:
let first = &orders[0];let text = greeting(&first.customer);let large = count_large(orders, 10_000);Use the borrowed form of the type: &str for String, &[T] for Vec<T>, &Path for PathBuf. See Borrowing and Strings.
When it is acceptable
Section titled “When it is acceptable”When the function stores or consumes the value, taking ownership is correct. A constructor is the usual example:
pub struct Customer { name: String, orders: Vec<Order>,}
impl Customer { /// Takes ownership because the struct stores the values. pub fn new(name: String, orders: Vec<Order>) -> Self { Self { name, orders } }}If the caller has a String that it no longer needs, it moves it in, and nothing is copied. Other correct cases: the function sends the value to another thread, or returns a changed version of it.
Some APIs take impl Into<String> or impl AsRef<str> to accept both forms. That is also fine.
How to detect it
Section titled “How to detect it”- Clippy
needless_pass_by_value(pedantic) finds parameters that are taken by value but only borrowed in the body. - In review: for each
String,Vec<T>,PathBuf, orHashMapparameter, ask “does the function keep it?” If not, it should borrow. - Look at the call sites.
.clone()or.to_vec()in the arguments usually means the callee should borrow.
What to tell your agent
Section titled “What to tell your agent”Change greeting to take name: &str and count_large to take orders: &[Order].Remove the .clone() and .to_vec() at the call sites. Only take String or Vecby value when the function stores or consumes the value.Related: S01: clone() to satisfy the borrow checker and S06: &String, &Vec<T>, &Box<T> parameters.