[Rust] Reading Input and Writing Output
When solving algorithm problems in Rust, I/O is usually the first wall you hit. It's not as straightforward as cin/cout in C++ or input()/print() in Python, but once you learn it, you can use the same pattern everywhere.
Basic Output: println!
fn main() {
println!("Hello World!");
}
The simplest way to print. Because it locks and flushes stdout on every call, it gets slow when you have a lot of output.
Fast Output: BufWriter
use std::io::{self, Write, BufWriter};
fn main() {
let stdout = io::stdout();
let mut out = BufWriter::new(stdout.lock());
writeln!(out, "Hello World!").unwrap();
}
Wrapping stdout in a BufWriter buffers everything and flushes it all at once. For problems with heavy output, this can be tens of times faster.
stdout.lock()→ acquires the stdout lock only onceBufWriter→ reduces the number of system calls via bufferingwriteln!→ the macro to use instead ofprintln!
Basic Input: read_line
use std::io;
fn main() {
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let n: i32 = input.trim().parse().unwrap();
println!("{}", n);
}
Fine for reading a single line, but tedious when you need to read multiple lines.
Fast Input: BufReader + read_to_string
use std::io::{self, BufReader, Read};
fn main() {
let mut input = String::new();
let mut reader = BufReader::new(io::stdin().lock());
reader.read_to_string(&mut input).unwrap();
let mut iter = input.split_whitespace();
let n: i32 = iter.next().unwrap().parse().unwrap();
}
read_to_string reads the entire input at once, up to EOF. When running directly in a terminal, you'll need to press Ctrl+Z (Windows) or Ctrl+D (Linux/Mac) after entering your input to signal EOF. On online judges like BOJ, EOF is sent automatically, so you don't need to worry about it.
split_whitespace() treats spaces, tabs, and newlines all as delimiters, making it easy to parse token by token.
Reading Two Numbers and Adding Them (BOJ 1000)
use std::io::{self, BufReader, BufWriter, Read, Write};
fn main() {
let mut input = String::new();
let mut reader = BufReader::new(io::stdin().lock());
reader.read_to_string(&mut input).unwrap();
let mut iter = input.split_whitespace();
let a: i32 = iter.next().unwrap().parse().unwrap();
let b: i32 = iter.next().unwrap().parse().unwrap();
let stdout = io::stdout();
let mut out = BufWriter::new(stdout.lock());
writeln!(out, "{}", a + b).unwrap();
}
What Is unwrap()?
Functions like writeln! and parse() return a Result type.
- Success (
Ok(value)) →unwrap()extracts the value - Failure (
Err(error)) →unwrap()panics and terminates the program
In algorithm problems, I/O almost never fails, so using unwrap() to keep things simple is fine. In production code, it's better to propagate errors with the ? operator.
What Are mut and &mut?
mut in variable declarations
In Rust, variables are immutable by default. You need mut to make them changeable.
let input = String::new(); // immutable → cannot be modified
let mut input = String::new(); // mutable → can be modified
Since read_to_string needs to write data into input, mut is required.
&mut when passing to functions
& is a reference — it borrows a value without copying it.
&input→ read-only reference (immutable reference)&mut input→ read + write reference (mutable reference)
reader.read_to_string(&mut input).unwrap();
// ^^^^^^^^^^
// We pass &mut because read_to_string needs to write into input
Since read_to_string needs to fill input with the data it reads, it requires a mutable reference via &mut.
Why Call lock()?
stdout() and stdin() can be accessed from multiple threads simultaneously, so by default they acquire and release a lock on every call.
// println! works roughly like this internally
stdout().lock() → write → unlock
stdout().lock() → write → unlock // slower the more times it runs
Calling .lock() directly lets you hold the lock once and keep using it.
let stdout = io::stdout();
let locked = stdout.lock(); // lock acquired once
let mut out = BufWriter::new(locked); // reused from here on
The same applies to stdin. Calling stdin().lock() once means no repeated lock/unlock cycles while reading, which is faster.
What Is next()?
split_whitespace() returns an iterator — an object that lets you pull values out one at a time, in order.
Each call to .next() returns the next value.
let input = "1 2 3";
let mut iter = input.split_whitespace();
iter.next() // → Some("1") first value
iter.next() // → Some("2") second value
iter.next() // → Some("3") third value
iter.next() // → None nothing left
Some and None are the Option type — they represent a value that may or may not exist. unwrap() extracts the value inside Some.
iter.next() // Some("1")
iter.next().unwrap() // "1" ← the value unwrapped from Some
What Is parse()?
parse() converts a string into a desired type. It's the equivalent of int() in Python or stoi() in C++.
let s = "42";
let n: i32 = s.parse().unwrap(); // "42" → 42 (i32)
The target type is inferred from the variable's type annotation.
let a: i32 = "42".parse().unwrap(); // 32-bit integer
let b: i64 = "42".parse().unwrap(); // 64-bit integer
let c: f64 = "3.14".parse().unwrap(); // 64-bit float
let d: usize = "42".parse().unwrap(); // unsigned integer (for array indices, etc.)
If the conversion fails, it returns Err, and unwrap() will panic.
let n: i32 = "abc".parse().unwrap(); // panic! not a number
Putting It All Together
Here's a step-by-step breakdown of parsing the input "1 2" into two integers:
let mut iter = input.split_whitespace();
// iter: an iterator that yields "1" and "2" in order
let a: i32 = iter.next().unwrap().parse().unwrap();
// ^^^^^^^^^^^ → Some("1")
// ^^^^^^^^^ → "1"
// ^^^^^^^ → Result<i32, _>
// ^^^^^^^^^ → 1 (i32)
Pull out a string with next() → unwrap Some with unwrap() → convert to a number with parse() → unwrap Result with unwrap().
Summary
| Slow | Fast | |
|---|---|---|
| Input | stdin().read_line() | BufReader + read_to_string |
| Output | println! | BufWriter + writeln! |
For competitive programming, just always use the BufReader + BufWriter combination.