구조체와 enum으로 데이터를 표현할 수 있게 되었다면, 이제 여러 개의 값을 한꺼번에 관리할 컬렉션이 필요합니다. Rust 표준 라이브러리에서 가장 자주 사용하는 컬렉션은 Vec<T>와 [[hashmap|HashMap<K, V>]]입니다. 이번 글에서는 학습 로그를 벡터와 해시맵에 담고, 반복 처리(iteration)로 요약을 만들어 봅니다.
Vec 기본기
Vec<T>는 크기가 가변적인 배열입니다. 힙에 데이터를 저장하고, 필요할 때 push, pop, iter 등을 사용할 수 있습니다.
fn main() {
let mut topics = Vec::new();
topics.push("ownership".to_string());
topics.push("borrowing".to_string());
topics.push("enum".to_string());
for topic in &topics {
println!("학습 예정: {}", topic);
}
}
for topic in &topics는 벡터의 각 요소에 대한 불변 참조를 순회합니다. 동시에 가변 참조를 만들지 않는 이상 안전하게 반복할 수 있습니다.
벡터와 구조체 연결하기
학습 로그 여러 개를 벡터로 관리하면, 간단한 리포트를 만들 수 있습니다.
struct StudyLog {
topic: String,
finished: bool,
}
fn main() {
let logs = vec![
StudyLog { topic: "borrowing".into(), finished: true },
StudyLog { topic: "structs".into(), finished: true },
StudyLog { topic: "enum".into(), finished: false },
];
let finished_count = logs.iter().filter(|log| log.finished).count();
println!("완료: {} / 총 {}건", finished_count, logs.len());
}
iter()는 불변 참조 이터레이터를 반환합니다. iter_mut()는 가변 참조 이터레이터로, 각 요소를 수정할 때 사용합니다.
fn main() {
let mut progress = vec![10, 40, 70];
for percent in progress.iter_mut() {
*percent += 5;
}
println!("업데이트된 진행률: {:?}", progress);
}
HashMap으로 요약 만들기
[[hashmap|HashMap<K, V>]]는 키-값 쌍을 저장합니다. 학습 항목별로 메모 개수를 기록하는 데 사용해 봅시다.
use std::collections::HashMap;
fn main() {
let mut notes: HashMap<String, Vec<String>> = HashMap::new();
notes.entry("borrowing".into()).or_default().push("불변 참조 여러 개 허용".into());
notes.entry("borrowing".into()).or_default().push("가변 참조는 하나".into());
notes.entry("enum".into()).or_default().push("match로 상태 분기".into());
for (topic, memos) in ¬es {
println!("{}: {}개 메모", topic, memos.len());
}
}
entry API는 키가 없으면 새 값을 만들고, 있으면 기존 값을 반환합니다. insert는 값을 바로 덮어쓸 때 간단하고, entry는 "없으면 만들고 있으면 이어서 수정"할 때 편합니다. or_default는 Default 구현을 호출해 빈 벡터를 만들어 주므로, 매번 insert 여부를 확인하지 않아도 됩니다.
HashMap과 enum 결합하기
이전 편에서 만든 Progress enum을 해시맵 값으로 사용하면, 각 주제가 어디까지 진행됐는지 한눈에 볼 수 있습니다.
use std::collections::HashMap;
#[derive(Debug)]
enum Progress {
Todo,
Doing(u8),
Done,
}
fn main() {
let mut status: HashMap<String, Progress> = HashMap::new();
status.insert("borrowing".into(), Progress::Done);
status.insert("structs".into(), Progress::Doing(50));
status.insert("collections".into(), Progress::Todo);
for (topic, progress) in &status {
match progress {
Progress::Todo => println!("{}: 시작 전", topic),
Progress::Doing(p) => println!("{}: {}% 진행", topic, p),
Progress::Done => println!("{}: 완료", topic),
}
}
}
불변 참조 이터레이터를 사용했으므로, 루프 안에서 status를 수정할 수 없습니다. 필요하면 iter_mut()로 가변 참조를 받아 수정한 뒤, 루프가 끝난 후에만 다른 연산을 수행하세요.
이터레이션 기본 패턴
for item in collection은 ownership을 이동시킵니다. 반복이 끝나면 원본을 다시 쓰지 못할 수 있습니다.for item in &collection또는collection.iter()는 불변 참조를 순회합니다.for item in &mut collection또는iter_mut()은 가변 참조를 순회합니다. 동시에 다른 참조를 만들 수 없다는 borrowing 규칙을 잊지 마세요.
짧게 기억하면 아래와 같습니다.
- 빌려서 보기:
&collection,iter() - 빌려서 수정하기:
&mut collection,iter_mut() - 통째로 가져가기:
collection
이터레이터는 체이닝 메서드(map, filter, collect)를 제공해, 앞으로 배울 클로저와도 자연스럽게 연결됩니다.
CodeSandbox로 이어서 실습하기
아래 샌드박스는 CodeSandbox의 Rust starter입니다. 이번 글의 핵심 코드를 src/main.rs에 옮기고, cargo check와 cargo run 결과를 나란히 보면서 컴파일 메시지와 실행 출력을 비교해 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요