Because usually in real-world programs you want to read more than one line (eg. in a loop one line at a time, or append multiple lines worth of data to the same string), and if so, you want to reuse the string to avoid unneeded allocations.
It's a common theme throughout rust std. The callers provide the buffer, providing more control over allocations. There's more ergonomic stuff like, io::stdin().lines().next(), which returns Option<io::Result<String>>
Thanks for that; I was in fact wondering how appending to a String was ergonomic, since in many cases those inputs will be independent and now we would have to extract them from the String.
So that argument would've been more convincing getting a Vec<String>, which is somewhat close to what lines() do.
As they have in the example, using the iterator:
use std::io::stdin;
fn main() {
for line in stdin().lines() {
println!("line: {}",line.unwrap());
}
}
which I wouldn't need if reading just one String (I'd use the one-liner you provided) but if needing to parse the separate answers.
Doesn't the same argument apply to every function that returns a String or a Vec or any other collection, that might be used in a loop?
Reusing recent allocations when possible should normally be the memory allocator's job. This API is basically saying: we want you to pre-allocate memory manually rather than use the memory allocator.
This should at best be a secondary, lower-level API for people that want more control over such details, rather than the default way to read a line. Especially since reading a line is such a common function in non performance critical settings, such as interactive programs.
That's how dynamically allocated data structures work. String reallocate when they grow past capacity. But reusing means you hit that capacity ceiling once, then coast. Creating new Strings means paying the allocation cost every single time.
use std::io::{Result, stdin};
fn main() -> Result<()> {
let mut buf = String::new();
// First read: might allocate/grow to say 100 bytes capacity
stdin().read_line(&mut buf)?; // len=50, capacity=100
buf.clear(); // len=0, capacity=100 (!!allocation kept!!)
// Second read: no allocation needed if line fits in 100 bytes
stdin().read_line(&mut buf)?; // reuses existing allocation
Ok(())
}
When you clear() a String (or Vec), the length goes to 0 but the capacity stays. The heap allocation is preserved.
So in a loop:
First few iterations might reallocate as the buffer grows
Once capacity stabilizes (usually quickly), zero allocations for remaining iterations
Compare to returning a new String each time:
// Allocates on EVERY iteration (THIS IS HYPOTHETICAL)
while let Ok(line) = reader.read_line_owned() { ... }