[Rust] 입력받고 출력하기
알고리즘 문제를 Rust로 풀 때 가장 먼저 부딪히는 게 입출력이다. C++의 cin/cout, Python의 input()/print()처럼 간단하진 않지만, 한번 익혀두면 계속 쓸 수 있다.
기본 출력: println!
fn main() {
println!("Hello World!");
}
가장 간단한 출력. 매 호출마다 stdout을 lock하고 flush하기 때문에 출력이 많으면 느리다.
빠른 출력: 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();
}
BufWriter로 stdout을 감싸면 버퍼에 모아서 한번에 출력한다. 출력이 많은 문제에서 수십 배 빠르다.
stdout.lock()→ stdout lock을 한 번만 잡음BufWriter→ 버퍼링으로 시스템 콜 횟수를 줄임writeln!→println!대신 사용하는 매크로
기본 입력: 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);
}
한 줄만 읽을 때 쓸 수 있다. 간단하지만 여러 줄 읽을 때는 번거롭다.
빠른 입력: 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은 EOF까지 전체 입력을 한번에 읽는다. 그래서 터미널에서 직접 실행할 때는 입력 후 Ctrl+Z(Windows) 또는 Ctrl+D(Linux/Mac)를 눌러야 한다. 백준 같은 온라인 저지에서는 자동으로 EOF가 들어오니 신경 쓸 필요 없다.
split_whitespace()는 공백, 탭, 줄바꿈을 모두 구분자로 처리해서 파싱이 편하다.
두 수 입력받아 더하기 (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();
}
unwrap()이 뭔데?
writeln!이나 parse() 같은 함수는 Result 타입을 반환한다.
- 성공 (
Ok(값)) →unwrap()이 값을 꺼내줌 - 실패 (
Err(에러)) →unwrap()이 panic으로 프로그램 종료
알고리즘 문제에서는 입출력이 실패할 일이 없으니 unwrap()으로 간단히 처리한다. 실제 프로덕션 코드에서는 ? 연산자로 에러를 전파하는 게 더 좋다.
mut와 &mut는 뭔데?
변수 선언할 때 mut
Rust는 변수가 기본적으로 불변(immutable)이다. 값을 바꾸려면 mut를 붙여야 한다.
let input = String::new(); // 불변 → 값 변경 불가
let mut input = String::new(); // 가변 → 값 변경 가능
read_to_string이 input에 데이터를 써넣어야 하니까 mut가 필요하다.
함수에 넘길 때 &mut
&는 참조(reference)다. 값을 복사하지 않고 빌려주는 것.
&input→ 읽기만 가능한 참조 (불변 참조)&mut input→ 읽기 + 쓰기 가능한 참조 (가변 참조)
reader.read_to_string(&mut input).unwrap();
// ^^^^^^^^^^
// input을 수정해야 하니까 &mut로 넘긴다
read_to_string은 읽은 데이터를 input 안에 채워넣어야 하므로 수정 권한이 있는 &mut로 넘겨야 한다.
lock()은 왜 하는 거야?
stdout()과 stdin()은 여러 스레드에서 동시에 접근할 수 있다. 그래서 기본적으로 매 호출마다 lock을 잡고 푼다.
// println!은 내부적으로 매번 이렇게 동작한다
stdout().lock() → 쓰기 → unlock
stdout().lock() → 쓰기 → unlock // 반복할수록 느림
.lock()을 직접 호출하면 lock을 한 번만 잡고 계속 쓸 수 있다.
let stdout = io::stdout();
let locked = stdout.lock(); // 한 번만 lock
let mut out = BufWriter::new(locked); // 이후 계속 사용
stdin도 마찬가지다. stdin().lock()으로 lock을 한 번 잡아두면 입력을 읽는 동안 반복적인 lock/unlock이 없어서 빠르다.
next()는 뭐야?
split_whitespace()는 iterator(반복자)를 반환한다. iterator는 값을 하나씩 순서대로 꺼낼 수 있는 객체다.
.next()를 호출할 때마다 다음 값을 하나씩 꺼낸다.
let input = "1 2 3";
let mut iter = input.split_whitespace();
iter.next() // → Some("1") 첫 번째 값
iter.next() // → Some("2") 두 번째 값
iter.next() // → Some("3") 세 번째 값
iter.next() // → None 더 이상 없음
Some과 None은 Option 타입이다. 값이 있을 수도, 없을 수도 있다는 뜻이다. unwrap()으로 Some 안의 값을 꺼낸다.
iter.next() // Some("1")
iter.next().unwrap() // "1" ← Some을 벗겨낸 값
parse()는 뭐야?
parse()는 문자열을 원하는 타입으로 변환하는 메서드다. Python의 int(), C++의 stoi()와 같은 역할이다.
let s = "42";
let n: i32 = s.parse().unwrap(); // "42" → 42 (i32)
변환할 타입은 변수의 타입 선언으로 지정한다.
let a: i32 = "42".parse().unwrap(); // 정수 32비트
let b: i64 = "42".parse().unwrap(); // 정수 64비트
let c: f64 = "3.14".parse().unwrap(); // 실수 64비트
let d: usize = "42".parse().unwrap(); // 부호 없는 정수 (배열 인덱스 등)
변환에 실패하면 Err를 반환하고, unwrap()이 panic을 일으킨다.
let n: i32 = "abc".parse().unwrap(); // panic! 숫자가 아님
전체 흐름 정리
입력 "1 2"를 두 정수로 파싱하는 과정을 한 줄씩 따라가 보면:
let mut iter = input.split_whitespace();
// iter: ["1", "2"]를 순서대로 꺼낼 수 있는 iterator
let a: i32 = iter.next().unwrap().parse().unwrap();
// ^^^^^^^^^^^ → Some("1")
// ^^^^^^^^^ → "1"
// ^^^^^^^ → Result<i32, _>
// ^^^^^^^^^ → 1 (i32)
next()로 문자열을 꺼내고 → unwrap()으로 Some을 벗기고 → parse()로 숫자로 변환하고 → unwrap()으로 Result를 벗긴다.
정리
| 느린 방법 | 빠른 방법 | |
|---|---|---|
| 입력 | stdin().read_line() | BufReader + read_to_string |
| 출력 | println! | BufWriter + writeln! |
알고리즘 문제 풀 때는 그냥 항상 BufReader + BufWriter 조합을 쓰자.