Rust 기초 강좌 #7 컬렉션과 이터레이터: Vec·String·HashMap과 for문을 대체하는 체인
2편에서 배열은 길이가 고정이라고 했고, 실무의 주인공은 따로 있다고 예고했습니다. 이번 편이 그 주인공들입니다. 가변 길이 목록 Vec, 문자열 String, 키-값 저장소 HashMap, 그리고 이 컬렉션들을 다루는 Rust다운 방식인 이터레이터입니다.
Vec: 실무의 기본 목록 #
let mut scores: Vec<u32> = Vec::new();
scores.push(90);
scores.push(85);
let scores = vec![90, 85, 72]; // 리터럴 매크로
Vec<T>는 힙에 데이터를 두는 가변 길이 배열입니다. 힙을 소유하므로 3편의 소유권 규칙이 그대로 적용되고, 4편에서 본 “순회 중 push 금지"의 컴파일 에러도 Vec이 무대였습니다. 요소 접근은 두 가지인데, 차이가 중요합니다.
let third = scores[2]; // 범위 밖이면 패닉
let third = scores.get(2); // 범위 밖이면 None (Option<&u32>)
인덱스가 확실히 유효한 로직에서는 [2]를 쓰고, 인덱스가 입력이나 계산에서 오는 지점에서는 get으로 받아 Option을 처리합니다. 5편과 6편에서 세운 경계(“일어나면 안 되는 상태는 패닉, 예상 가능한 실패는 타입으로”)가 접근 방법 선택에도 그대로 이어지는 것입니다.
String: 인덱스가 안 되는 이유 #
Rust의 String은 UTF-8 바이트의 Vec입니다. 그래서 다른 언어에서 당연하던 코드가 컴파일되지 않습니다.
let title = String::from("러스트 강좌");
let first = title[0]; // 컴파일 에러: String은 인덱스 접근 불가
2편에서 char는 4바이트 유니코드라고 했고, 4편에서는 슬라이스로 글자 중간을 자르면 패닉이 난다고 예고했습니다. 여기서 답이 모입니다. UTF-8에서 한글 한 글자는 3바이트라서 title[0]이 “첫 글자"라는 보장이 없습니다. 바이트 하나는 글자가 아니기 때문입니다. Rust는 “대충 첫 바이트를 돌려주는” 대신 이 연산 자체를 금지하고, 무엇을 원하는지 명시하게 합니다.
let first_char = title.chars().next(); // Some('러') — 문자 단위
let byte_len = title.len(); // 16 — 바이트 단위 길이
let char_count = title.chars().count(); // 5 — 문자 수
len()이 글자 수가 아니라 바이트 수라는 점은 입력 길이 제한 로직에서 흔한 함정이니 기억해 둘 가치가 있습니다. 문자열을 만들 때는 push_str과 format! 매크로가 기본 도구입니다.
HashMap: 키-값과 entry API #
use std::collections::HashMap;
let mut stock: HashMap<String, u32> = HashMap::new();
stock.insert(String::from("키보드"), 12);
let count = stock.get("키보드"); // Option<&u32>
조회가 Option인 것은 이제 놀랍지 않을 것입니다. 없는 키는 null이 아니라 None입니다. HashMap에서 알아 둘 관용구는 “없으면 넣고, 있으면 고치기"를 한 번의 조회로 처리하는 entry API입니다.
let mut word_count: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
*word_count.entry(word).or_insert(0) += 1;
}“키가 있는지 확인 → 없으면 삽입 → 다시 조회해서 수정"이라는 3단계가 한 줄이 됩니다. 단어 빈도 세기 같은 집계 코드의 표준형입니다.
이터레이터: for를 대체하는 체인 #
세 컬렉션을 배웠으니, 이들을 다루는 Rust다운 방식을 보겠습니다. “80점 이상 점수의 합"을 두 가지로 써 봅니다.
// for 방식
let mut total = 0;
for s in &scores {
if *s >= 80 {
total += s;
}
}
// 이터레이터 체인 방식
let total: u32 = scores.iter().filter(|s| **s >= 80).sum();filter에 넘긴 |s| ...는 클로저(익명 함수)입니다. 체인 방식이 관용구인 이유는 취향이 아니라 구조입니다. mut 변수가 사라지고, “걸러서 더한다"는 의도가 구현 순서가 아니라 이름으로 드러납니다. 변환에는 map, 결과를 새 컬렉션으로 모을 때는 collect를 씁니다.
let names: Vec<String> = users.iter()
.filter(|u| u.active)
.map(|u| u.name.clone())
.collect();두 가지를 더 알아 두면 완성입니다. 첫째, 이터레이터는 지연 평가됩니다. map과 filter는 계획만 쌓고, collect나 sum 같은 소비 지점에서 한 번에 실행되므로 중간 컬렉션이 만들어지지 않습니다. 컴파일 후 성능은 손으로 쓴 반복문과 대등해서, 표현력을 위해 성능을 내주는 거래가 아닙니다. 둘째, 순회에도 소유권 구분이 있습니다. iter()는 참조로 빌려서 순회하고, into_iter()는 소유권을 가져가며 순회합니다. 순회 후에도 컬렉션을 계속 사용할 것이면 iter()가 기본값입니다.
정리 #
Vec이 실무의 기본 목록입니다. 확실한 인덱스는[i], 밖에서 온 인덱스는get으로 받아Option을 처리합니다.String은 UTF-8 바이트의 Vec이라서 인덱스 접근이 금지됩니다. 문자 단위는chars(),len()은 바이트 수라는 것이 실무 함정입니다.HashMap조회는Option이고, 집계는entry(...).or_insert(...)관용구가 표준형입니다.- 컬렉션 처리의 기본형은 for보다
iter().filter().map().collect()체인입니다. 지연 평가라서 중간 비용 없이 손으로 쓴 반복문과 대등하게 컴파일됩니다. - 다음 편은 트레이트와 제네릭입니다. 지금까지 미뤄 온
derive(Debug),Copy트레이트,Box<dyn Error>의 정체가 한 번에 풀립니다.