Skip to content

Latest commit

 

History

History
487 lines (370 loc) · 12.7 KB

05.md

File metadata and controls

487 lines (370 loc) · 12.7 KB

Rust 문법 로드맵 — 5단계: 컬렉션 & 이터레이터 (상세 교재)

이 문서는 로드맵 5단계를 코드 예제와 함께 상세히 풀어놓은 교재입니다. 실무 Rust 코드에서 가장 자주 쓰는 문법 묶음입니다. 소유권 관점(iter / into_iter / iter_mut)을 먼저 고정한 뒤, 어댑터 체인과 커스텀 이터레이터로 갑니다.

선수: 2단계 소유권, 4단계 Result 체이닝


5.1 컬렉션 기본 API — Vec / HashMap / HashSet / BTreeMap

Vec<T> — 가변 길이 배열

fn main() {
    let mut v: Vec<i32> = Vec::new();
    v.push(1);
    v.push(2);

    let mut v = vec![1, 2, 3]; // 매크로 — 타입·용량 추론
    v.extend([4, 5]);

    println!("len={}, cap={}", v.len(), v.capacity());
    println!("첫 원소 = {}", v[0]);          // 없으면 panic
    println!("안전한 접근 = {:?}", v.get(10)); // Option<&T>

    // 슬라이스로 함수에 넘기기 (2단계 복습)
    print_slice(&v);
    print_slice(&v[1..3]);
}

fn print_slice(s: &[i32]) {
    println!("{s:?}");
}

자주 쓰는 API:

메서드 역할
push / pop 끝에서 추가·제거
insert / remove 중간 — O(n), 자주 쓰면 다른 구조 검토
extend / append 여러 원소 합치기
retain 조건에 맞는 것만 남기기
drain(..) 구간을 빼내며 이터레이터로
split_at_mut 겹치지 않는 가변 슬라이스 둘
fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    v.retain(|&x| x % 2 == 1);
    println!("{v:?}"); // [1, 3, 5]

    let mut v = vec![10, 20, 30, 40];
    let drained: Vec<_> = v.drain(1..3).collect();
    println!("drained={drained:?}, left={v:?}"); // [20,30], [10,40]
}

HashMap<K, V> — 해시 맵

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Red"), 50);

    // 조회
    let s = scores.get("Blue"); // Option<&i32>
    let s = scores.get("Blue").copied().unwrap_or(0);

    // entry API — 없으면 넣고, 있으면 수정 (실무 단골)
    scores.entry(String::from("Yellow")).or_insert(30);
    let blue = scores.entry(String::from("Blue")).or_insert(0);
    *blue += 5;

    for (k, v) in &scores {
        println!("{k}: {v}");
    }
}

entry 패턴으로 단어 빈도:

use std::collections::HashMap;

fn word_count(text: &str) -> HashMap<&str, usize> {
    let mut map = HashMap::new();
    for w in text.split_whitespace() {
        *map.entry(w).or_insert(0) += 1;
    }
    map
}

키는 Eq + Hash가 필요합니다. String/i32/&str(수명 주의) 등이 흔합니다.

HashSet<T> — 중복 없는 집합

use std::collections::HashSet;

fn main() {
    let mut set = HashSet::from([1, 2, 3]);
    set.insert(2); // false — 이미 있음
    set.insert(4);

    println!("{}", set.contains(&3));

    let a = HashSet::from([1, 2, 3]);
    let b = HashSet::from([2, 3, 4]);
    println!("{:?}", a.intersection(&b).collect::<Vec<_>>()); // 2, 3
    println!("{:?}", a.union(&b).collect::<Vec<_>>());
    println!("{:?}", a.difference(&b).collect::<Vec<_>>());
}

BTreeMap / BTreeSet — 정렬된 맵·집합

use std::collections::BTreeMap;

fn main() {
    let mut map = BTreeMap::new();
    map.insert(3, "c");
    map.insert(1, "a");
    map.insert(2, "b");

    // 키 순서 보장 — 순회·range에 유리
    for (k, v) in &map {
        println!("{k}: {v}");
    }

    for (k, v) in map.range(2..) {
        println!("range {k}: {v}");
    }
}
HashMap BTreeMap
순서 없음(랜덤화) 키 정렬
평균 연산 O(1) O(log n)
키 요구 Eq + Hash Ord
언제 기본값, 빈도수 범위 쿼리, 안정적 순서

체크포인트

  • Vec에서 get vs 인덱싱 차이를 panic으로 체감
  • entry().or_insert로 문자 빈도수 맵 만들기
  • 같은 데이터를 HashMap과 BTreeMap에 넣고 순회 순서 비교

5.2 iter vs into_iter vs iter_mut — 소유권

컬렉션을 순회하는 세 가지 길이 무엇을 빌리거나 소비하느냐입니다.

fn main() {
    let mut v = vec![String::from("a"), String::from("b")];

    // 1) iter() → Iterator<Item = &T>  (불변 빌림)
    for s in v.iter() {
        println!("len={}", s.len());
    }
    // v 여전히 사용 가능

    // 2) iter_mut() → Iterator<Item = &mut T>
    for s in v.iter_mut() {
        s.push('!');
    }

    // 3) into_iter() → Iterator<Item = T>  (소유권 이동, v 소비)
    for s in v.into_iter() {
        println!("{s}");
    }
    // println!("{v:?}"); // 에러 — moved
}

for x in collection은 **IntoIterator**를 호출합니다.

표현 대략 동일 Item
for x in &v v.iter() &T
for x in &mut v v.iter_mut() &mut T
for x in v v.into_iter() T

HashMap도 같습니다:

use std::collections::HashMap;

fn main() {
    let map = HashMap::from([("a", 1), ("b", 2)]);

    for (k, v) in &map {          // (&K, &V)
        println!("{k}: {v}");
    }
    for (k, v) in map {           // (K, V) — map 소비
        println!("{k}: {v}");
    }
}

체크포인트

  • 같은 Vec<String>에 세 가지 순회를 쓰고, 어떤 게 소비인지 확인
  • for x in v 후 v를 써서 move 에러를 읽어보기

5.3 어댑터 체이닝

이터레이터는 lazy입니다. collect / sum / for / count 같은 consuming adapter를 만나야 실제로 돌아갑니다.

fn main() {
    let v = vec![1, 2, 3, 4, 5, 6];

    let result: Vec<i32> = v
        .iter()
        .filter(|x| *x % 2 == 0)
        .map(|x| x * 10)
        .collect();

    println!("{result:?}"); // [20, 40, 60]
}

자주 쓰는 어댑터

fn main() {
    let v = vec!["10", "x", "20", "30"];

    // map — 각 원소 변환
    let lens: Vec<_> = v.iter().map(|s| s.len()).collect();

    // filter — 조건
    let even: Vec<_> = (0..10).filter(|x| x % 2 == 0).collect();

    // filter_map — filter + map + Option 펼치기
    let nums: Vec<i32> = v
        .iter()
        .filter_map(|s| s.parse().ok())
        .collect();

    // enumerate — (index, item)
    for (i, x) in nums.iter().enumerate() {
        println!("{i}: {x}");
    }

    // zip — 두 이터레이터를 쌍으로 (짧은 쪽에 맞춤)
    let a = [1, 2, 3];
    let b = ["a", "b"];
    let pairs: Vec<_> = a.iter().zip(b.iter()).collect();
    println!("{pairs:?}");

    // flat_map — 하나 → 여러 개로 펼침
    let words = ["hello world", "rust lang"];
    let tokens: Vec<&str> = words.iter().flat_map(|s| s.split_whitespace()).collect();

    // fold / reduce — 누적
    let sum = (1..=10).fold(0, |acc, x| acc + x);
    let sum2: i32 = (1..=10).sum(); // 전용 메서드

    // take / skip / chain
    let first3: Vec<_> = (0..100).take(3).collect();
    let skipped: Vec<_> = (0..5).skip(2).collect();
    let chained: Vec<_> = (0..2).chain(10..12).collect();

    let _ = (lens, even, tokens, sum, sum2, first3, skipped, chained);
}

collect와 타입 명시

collect의 반환 타입은 여러 가능성이 있어 종종 명시가 필요합니다.

fn main() {
    let v = vec![1, 2, 3];

    let a: Vec<_> = v.iter().map(|x| x * 2).collect();
    let b = v.iter().map(|x| x * 2).collect::<Vec<_>>();
    let c: std::collections::HashSet<_> = v.iter().copied().collect();

    // Result를 collect하면 하나라도 Err면 전체가 Err — 실무 보석
    let parsed: Result<Vec<i32>, _> = ["1", "2", "x"]
        .iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("{parsed:?}"); // Err(...)

    let _ = (a, b, c);
}

collect::<Result<Vec<_>, _>>() 패턴은 4단계 ?와 자주 짝을 이룹니다.

체크포인트

  • filter + map + collect로 짝수만 제곱한 Vec 만들기
  • filter_map으로 파싱 가능한 줄만 모으기
  • map(parse).collect::<Result<Vec<_>, _>>() 성공/실패 케이스 확인

5.4 커스텀 이터레이터 — Iterator 직접 구현

필수 구현은 type Item과 fn next(&mut self) -> Option<Self::Item> 하나면 됩니다. 나머지 어댑터(map 등)는 자동으로 따라옵니다.

struct Counter {
    current: usize,
    max: usize,
}

impl Counter {
    fn new(max: usize) -> Self {
        Self { current: 0, max }
    }
}

impl Iterator for Counter {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.max {
            let v = self.current;
            self.current += 1;
            Some(v)
        } else {
            None
        }
    }
}

fn main() {
    let sum: usize = Counter::new(5).sum(); // 0+1+2+3+4 = 10
    let evens: Vec<_> = Counter::new(10).filter(|x| x % 2 == 0).collect();
    println!("sum={sum}, evens={evens:?}");
}

슬라이스/범위를 감싼 이터레이터:

struct Windows<'a, T> {
    slice: &'a [T],
    size: usize,
    index: usize,
}

impl<'a, T> Windows<'a, T> {
    fn new(slice: &'a [T], size: usize) -> Self {
        Self {
            slice,
            size,
            index: 0,
        }
    }
}

impl<'a, T> Iterator for Windows<'a, T> {
    type Item = &'a [T];

    fn next(&mut self) -> Option<Self::Item> {
        if self.index + self.size > self.slice.len() {
            return None;
        }
        let window = &self.slice[self.index..self.index + self.size];
        self.index += 1;
        Some(window)
    }
}

fn main() {
    let data = [1, 2, 3, 4, 5];
    for w in Windows::new(&data, 3) {
        println!("{w:?}");
    }
    // 표준: data.windows(3) 과 같은 아이디어
}

체크포인트

  • Countdown(n)이터레이터: n, n-1, …, 1 후 None
  • 그 위에 .map(|x| x * x).collect::<Vec<_>>() 체이닝

5.5 종합 실습 — for 없이 단어 빈도수

로드맵 실습: 파일(또는 문자열)에서 읽은 텍스트를 이터레이터 체인만으로 단어 빈도 집계.

use std::collections::HashMap;
use std::fs;

fn word_frequencies(text: &str) -> HashMap<String, usize> {
    text
        .split_whitespace()
        .map(|w| {
            w.chars()
                .filter(|c| c.is_alphanumeric())
                .flat_map(|c| c.to_lowercase())
                .collect::<String>()
        })
        .filter(|w| !w.is_empty())
        .fold(HashMap::new(), |mut acc, word| {
            *acc.entry(word).or_insert(0) += 1;
            acc
        })
}

/// 빈도 내림차순 Top N
fn top_n(freq: &HashMap<String, usize>, n: usize) -> Vec<(&String, &usize)> {
    let mut items: Vec<_> = freq.iter().collect();
    items.sort_by(|a, b| b.1.cmp(a.1).then_with(|| a.0.cmp(b.0)));
    items.into_iter().take(n).collect()
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 파일이 있으면 읽고, 없으면 샘플 사용
    let text = fs::read_to_string("input.txt").unwrap_or_else(|_| {
        String::from(
            "Rust rust RUST is fun. Iterators in Rust are lazy and powerful. Fun fun fun!",
        )
    });

    let freq = word_frequencies(&text);

    // for 루프 없이 출력도 체인으로
    top_n(&freq, 5)
        .iter()
        .enumerate()
        .for_each(|(i, (word, count))| {
            println!("{}. {word} — {count}", i + 1);
        });

    Ok(())
}

포인트:

  1. split → map 정규화 → filter → fold/entry — 루프 없이 집계.
  2. 정규화에서 flat_map(to_lowercase) — 유니코드 소문자가 여러 char일 수 있음.
  3. sort_by + take로 Top N.
  4. 파일 IO는 4단계 Result/unwrap_or_else와 연결.

for_each는 사이드이펙트용 consuming adapter입니다. 값을 모을 땐 collect가 맞습니다.

확장 과제

  • BTreeMap으로 바꿔 알파벳 순 출력
  • 최소 길이 N 미만 단어 제외 (filter)
  • lines().flat_map(split_whitespace)로 줄 단위 파일 스트리밍 느낌 내기
  • 파싱 실패 줄을 filter_map으로 건너뛰는 로그 카운터 버전

이 단계 한 장 요약

Vec / HashMap / HashSet / BTreeMap
iter(&T)  iter_mut(&mut T)  into_iter(T)

Iterator = lazy
  adapters: map filter filter_map flat_map zip enumerate take fold …
  consume:  collect sum count for_each any all …

collect::<Vec<_>>() / collect::<Result<Vec<_>, _>>()
custom: impl Iterator { type Item; fn next }

다음 단계 예고

6단계 클로저에서는 |x| ... 문법과 Fn / FnMut / FnOnce, move 클로저를 다룹니다. 이 단계의 map/filter에 넘긴 그 인자들의 정체입니다.