S32: String building with + in a loop, format! used only to convert
What it looks like
Section titled “What it looks like”pub fn report(orders: &[Order]) -> String { let mut text = String::new(); for order in orders { text = text + &format!("{}", order.id) + ": " + &order.customer.to_string(); text = text + &format!(" ({} cents)\n", order.total_cents.to_string()); } text}
pub fn order_key(order: &Order) -> String { format!("{}", order.id)}Why AI agents produce it
Section titled “Why AI agents produce it”- It is Java’s
+=loop. In Java,text += id + ": " + customeris the first thing that people write, and it works. The agent translates it with+andformat!. format!("{}", x)works for every type, so it is a safe guess for “convert to a string”. The agent does not need to know thatto_string()exists..to_string()is added to make types match.+needs a&stron the right, and the agent adds conversions until the compiler accepts it.
Why it is a problem
Section titled “Why it is a problem”- Extra allocations. In the first line of the loop,
format!("{}", order.id)allocates a newString, andorder.customer.to_string()copies a name that was already a string. Each+can grow the buffer oftextand copy it. None of this is necessary. - Noise.
format!(" ({} cents)\n", order.total_cents.to_string())converts the number to a string, and then formats the string. The reader must check each conversion to see that it does nothing. - The text format is hard to see. The line format is spread across two statements and four
+operators. With onewriteln!, the format is one string.
The cost is small for a short report. It grows with the size of the data, and in a loop that runs for every request, it adds up. Do not guess: measure before you call it a performance problem. In a review, it is mainly a readability finding.
The better version
Section titled “The better version”Worse
+withformat!andto_string()in a loop, andformat!("{}", x)to convert a number.pub fn report(orders: &[Order]) -> String {let mut text = String::new();for order in orders {text = text + &format!("{}", order.id) + ": " + &order.customer.to_string();text = text + &format!(" ({} cents)\n", order.total_cents.to_string());}text}pub fn order_key(order: &Order) -> String {format!("{}", order.id)}BetterOne
writeln!for each line into the same buffer, andto_string()to convert.pub fn report(orders: &[Order]) -> String {let mut text = String::new();for order in orders {// `writeln!` on a `String` cannot fail, so `expect` documents an invariant.writeln!(text,"{}: {} ({} cents)",order.id, order.customer, order.total_cents).expect("writing to a String never fails");}text}pub fn order_key(order: &Order) -> String {order.id.to_string()}
write! and writeln! format directly into an existing String, with no temporary String. They need use std::fmt::Write. Writing to a String cannot fail, but write! returns a fmt::Result for all writers, so the result must be handled: an expect with a message is the usual choice. Some teams prefer let _ = writeln!(...) here. Both are fine, and the choice should be the same in the whole codebase.
For lists, join does it all:
pub fn customer_list(orders: &[Order]) -> String { orders .iter() .map(|o| o.customer.as_str()) .collect::<Vec<_>>() .join(", ")}When it is acceptable
Section titled “When it is acceptable”- One
+outside a loop is clear, and it reuses the buffer of the left string:
/// One `+` outside a loop is clear and allocates no more than `format!`.pub fn with_suffix(name: String) -> String { name + " (VIP)"}format!with a real template, such asformat!("{customer}.csv"), is the clearest way to build a short string.- Code that runs once, for example the text of a startup error, does not need optimization. Still prefer the clearer form.
How to detect it
Section titled “How to detect it”- Clippy
string_add(restriction group, off by default) reports+on aString. Clippystring_add_assign(pedantic group) reportss = s + .... - Clippy
to_string_in_format_args(performance group, on by default) reports.to_string()insideformat!arguments. Clippyimplicit_clone(pedantic group) reports.to_string()on a value that is already aString. - No default lint reports
format!("{}", x)used only to convert a number. Search forformat!("{}",with a single argument. - In a review, look for
String::new()before a loop, and+orformat!inside it.
What to tell your agent
Section titled “What to tell your agent”In report, build the text with one writeln! call for each order, into thesame String (use std::fmt::Write). Remove the format!("{}", ...) and.to_string() calls that only convert. In order_key, use order.id.to_string()instead of format!. Use join for lists of names.Related: S05: Owned parameters (String, Vec<T>) where a borrow is sufficient, and Strings.