We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Collections, Text and First Iterators step 3 of 21
String building: push_str, write!, and the 590x cliff
Render one line of CSV, with RFC 4180 quoting.
pub fn render_csv_line(fields: &[String]) -> String
Join the fields with commas. A field must be quoted if it contains a
comma, a double quote, a carriage return or a newline; inside quotes, every
" is written twice. Every other field is copied through untouched.
["a", "b", "c"] -> a,b,c
["a,b", "c"] -> "a,b",c
[he said "hi"] -> "he said ""hi"""
["", ""] -> ,
[] -> (empty string)
There is also a hidden case that builds 5000 fields and asserts that your
function performs at most 20 heap allocations while producing the line.
That budget is not tight — it comfortably allows a single output String
growing by doubling, which is 13 allocations for this input. What it does not
allow is one allocation per field.
First: the error in the starter
The starter reaches for write!, which is the right instinct. It does not
compile:
error[E0599]: cannot write into `String`
help: the method is available for `String` here:
use std::fmt::Write;
This is a genuinely confusing message the first time, and it is worth
understanding rather than pattern-matching. write!(dst, ...) expands to
dst.write_fmt(format_args!(...)). There are two traits in the standard
library with a write_fmt method:
-
std::io::Write— for byte sinks (files, sockets,Vec<u8>, stdout). Its methods returnio::Result, because writing to a socket can fail. -
std::fmt::Write— for text sinks (String,fmt::Formatter). Its methods returnfmt::Result, because formatting into memory can only fail if the sink says so.
A trait’s methods only exist on a type when the trait is in scope. So the
fix is use std::fmt::Write; at the top of your function or module — and
once you understand that, the whole family of “no method named X found” errors
on a type you know has X becomes a one-second diagnosis: the trait is not
imported.
(std::io::Write is the wrong one here and would give you a different error,
because String is not a byte sink. That pair of near-identical traits with
the same method names is the single most common import mix-up in Rust.)
Second: the belief you probably imported
Most people arrive in Rust having been told, correctly, that in Python or
Java s = s + t in a loop is quadratic, because strings are immutable and
every + copies the whole accumulated result. So they arrive suspicious of
+ and confident about “use a builder”.
In Rust that belief is wrong, and the reason is a signature. String‘s
Add impl is:
impl Add<&str> for String {
fn add(mut self, other: &str) -> String { self.push_str(other); self }
}
It takes self by value. There is no copy of the accumulated buffer — it
moves the existing String in, appends into its existing capacity, and moves
it back out. s = s + &t is push_str with different syntax.
Measured over 20 000 parts:
s = s + p 0.047 ms
s.push_str(p) 0.048 ms
with_capacity + push_str 0.047 ms
parts.concat() 0.034 ms
s = format!("{s}{p}") 28.100 ms <-- 590x
There is the real cliff, and it is hiding behind syntax that looks equivalent
to the line above it. format! allocates a brand new String every
iteration and copies everything accumulated so far into it. That is the
quadratic pattern people came looking for, and it is the one they write when
they are trying to avoid +.
On this problem’s 5000-field input the same shape costs 17 909 allocations
against a budget of 20. Note what did not happen: clippy -D warnings
passes the quadratic version happily. No lint saves you. The only thing that
catches it is the measurement.
An honest footnote: clippy’s string_add lint still flags s = s + p on
style grounds, even though the benchmark exonerates it on performance. It
is not in the default set, so it will not fail you here — but if you meet it
in a codebase that enables it, now you know the difference between “this is
slow” and “we prefer the other spelling”.
::: question format!("{x}") versus x.to_string() for a single u32 — same thing?
Same result, different cost. Measured over 200 000 conversions:
format!("{x}") takes 3.80 ms, x.to_string() 2.12 ms — about 1.8× slower.
format! builds a fmt::Arguments, walks a format-string parse tree and
dispatches through Display; to_string on an integer goes through a
specialised path.
clippy has useless_format for the extreme case of format!("{}", s) where
s is already a string. For the integer case there is no lint — it is a
judgement call, and it only matters in a hot loop.
:::
Which builder to reach for
-
Appending in a loop:
push_str/push. Clear, and as fast as anything. -
A slice of strings joined by nothing:
parts.concat(). Joined by a separator:parts.join(", "). Both pre-compute the total length and allocate exactly once — that is whyconcatwins the benchmark above. -
Interpolating values into a growing buffer:
write!(&mut buf, "...")withuse std::fmt::Write. Neverbuf += &format!(...). -
Building the string fresh, once, from a template:
format!. It is only bad in the accumulator position.
Remember the grade is compile + tests + clippy -D warnings.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.