Rust 사용자 정의 반복자
반복자 페이지에서는 표준 라이브러리가 주는 반복자를 써 봤어요. vec.iter(), .map(), .filter(), .sum() 같은 것들이요. 그런데 반복자는 직접 만들 수도 있어요. 카운트다운 타이머, 피보나치 수, 보고서의 페이지, 나만의 파일 형식의 줄들처럼, 값을 하나씩 건네주는 것이라면 무엇이든 반복자가 될 수 있어요.
좋은 소식이 있어요. 직접 쓰는 메서드는 next 하나뿐이에요. 그러면 Rust가 map, filter, take, sum, zip을 비롯해 수십 가지 어댑터를 공짜로 줘요. 번호표 발급기를 떠올려 보세요. 내가 만드는 건 다음 번호표를 뽑아 주는 부분뿐이에요. 대기 줄도, 화면도, “지금 부르는 번호” 안내판도 모두 알아서 돌아가요.
첫 번째 반복자: 카운트다운
섹션 제목: “첫 번째 반복자: 카운트다운”어떤 타입을 반복자로 만들려면 Iterator 트레이트를 구현하면 돼요. 필요한 건 두 가지예요.
type Item: 반복자가 건네주는 값의 타입이에요(연관 타입은 고급 트레이트 페이지에서 더 자세히 다뤄요).fn next(&mut self) -> Option<Self::Item>: 다음 값이 있으면Some(값)을, 더 없으면None을 돌려줘요.
struct Countdown { current: u32,}
impl Countdown { fn new(from: u32) -> Self { Countdown { current: from } }}
impl Iterator for Countdown { type Item = u32;
fn next(&mut self) -> Option<u32> { if self.current == 0 { None // 끝났어요 } else { let value = self.current; self.current -= 1; Some(value) } }}
fn main() { let mut countdown = Countdown::new(3);
println!("{:?}", countdown.next()); println!("{:?}", countdown.next()); println!("{:?}", countdown.next()); println!("{:?}", countdown.next()); println!("{:?}", countdown.next());
for n in Countdown::new(3) { println!("{}...", n); } println!("발사!");}실행 결과:
Some(3)Some(2)Some(1)NoneNone3...2...1...발사!눈여겨볼 점이 몇 가지 있어요.
next는 반복자를 바꾸기 때문에&mut self를 받아요. 구조체가 지금 어디까지 왔는지(current)를 기억하고, 호출할 때마다 한 칸씩 나아가요. 그래서countdown은let mut이어야 해요.None을 돌려주면 “다 끝났어요”라는 뜻이에요.for반복문은next를 계속 호출하다가 처음None이 나오면 멈춰요.- 이 카운트다운은 끝난 뒤에도 계속
None을 돌려줘요. 예의 바른 동작이지만, 트레이트가 요구하는 건 아니에요. 어떤 반복자는 다시 값을 내놓기 시작할 수도 있어요. 그런 보장이 필요하다면 어떤 반복자든.fuse()를 호출하면 돼요.
끝나지 않는 반복자: 피보나치
섹션 제목: “끝나지 않는 반복자: 피보나치”반복자가 꼭 끝나야 하는 건 아니에요. 피보나치 수열(0, 1, 1, 2, 3, 5, 8, …)은 영원히 이어져요. 각 수는 바로 앞 두 수의 합이에요. 반복자는 마지막 두 수만 기억하면 돼요.
struct Fibonacci { current: u64, next: u64,}
impl Fibonacci { fn new() -> Self { Fibonacci { current: 0, next: 1 } }}
impl Iterator for Fibonacci { type Item = u64;
fn next(&mut self) -> Option<u64> { let value = self.current; // checked_add는 오버플로가 나면 None을 돌려주고, `?`가 그 None을 그대로 넘겨요. // 그래서 수가 너무 커지면 프로그램이 죽는 대신 반복자가 끝나요. let new_next = self.current.checked_add(self.next)?; self.current = self.next; self.next = new_next; Some(value) }}
fn main() { let first_ten: Vec<u64> = Fibonacci::new().take(10).collect(); println!("{:?}", first_ten);}실행 결과:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]핵심은 .take(10)이에요. 이게 없으면 collect()가 수열 전체를 모으려고 할 거예요. 끝나지 않는 반복자에는 항상 take, take_while, find 같은 것으로 한계를 정해 주세요.
모든 어댑터가 공짜
섹션 제목: “모든 어댑터가 공짜”가장 좋은 부분이에요. Iterator 트레이트에는 메서드가 75개쯤 있는데, next를 뺀 나머지는 모두 next를 바탕으로 한 기본 구현이 있어요. 그래서 next를 쓰는 순간, 내 타입도 그 메서드를 전부 쓸 수 있어요.
struct Countdown { current: u32,}
impl Countdown { fn new(from: u32) -> Self { Countdown { current: from } }}
impl Iterator for Countdown { type Item = u32;
fn next(&mut self) -> Option<u32> { if self.current == 0 { None } else { let value = self.current; self.current -= 1; Some(value) } }}
fn main() { let total: u32 = Countdown::new(5).sum(); println!("합계: {}", total);
let evens: Vec<u32> = Countdown::new(10).filter(|n| n % 2 == 0).collect(); println!("짝수: {:?}", evens);
let labels: Vec<String> = Countdown::new(3).map(|n| format!("T-{}", n)).collect(); println!("라벨: {:?}", labels);
let multiple = Countdown::new(100).find(|n| n % 7 == 0); println!("처음 나온 7의 배수: {:?}", multiple);
let pairs: Vec<(u32, char)> = Countdown::new(3).zip(['a', 'b', 'c']).collect(); println!("zip 결과: {:?}", pairs);}실행 결과:
합계: 15짝수: [10, 8, 6, 4, 2]라벨: ["T-3", "T-2", "T-1"]처음 나온 7의 배수: Some(98)zip 결과: [(3, 'a'), (2, 'b'), (1, 'c')]Rust 트레이트가 왜 그렇게 쓸모 있는지 잘 보여 주는 예예요. 작은 메서드 하나를 넣으면, 커다란 도구 상자가 나와요.
반복자는 게을러요
섹션 제목: “반복자는 게을러요”map이나 take 같은 어댑터는 그 자체로는 아무 일도 하지 않아요. “계획”을 세워 둘 뿐이에요. 실제 일은 무언가가 반복자를 소비할 때만 일어나요. for 반복문, collect, sum 같은 것들이요. 이걸 지연 평가(lazy evaluation)라고 해요. next가 호출될 때마다 출력하는 반복자로 직접 확인해 볼게요.
struct Noisy { n: u32,}
impl Iterator for Noisy { type Item = u32;
fn next(&mut self) -> Option<u32> { self.n += 1; println!(" next() 호출됨, {} 돌려줌", self.n); Some(self.n) }}
fn main() { println!("체인을 만드는 중..."); let chain = Noisy { n: 0 }.map(|x| x * 10).take(3); println!("아직 아무것도 실행되지 않았어요. 이제 소비할게요:");
for value in chain { println!("받은 값: {}", value); } println!("끝");}실행 결과:
체인을 만드는 중...아직 아무것도 실행되지 않았어요. 이제 소비할게요: next() 호출됨, 1 돌려줌받은 값: 10 next() 호출됨, 2 돌려줌받은 값: 20 next() 호출됨, 3 돌려줌받은 값: 30끝여기서 배울 점이 두 가지예요. 첫째, 체인을 만들 때는 아무것도 출력되지 않았어요. next()가 한 번도 호출되지 않은 거예요. 둘째, 값은 하나씩 흘러가요. next 한 번, map 한 번, “받은 값” 한 번, 그다음에 다음 값이에요. 중간에 숨은 리스트 같은 건 없어요. 그리고 Noisy는 절대 끝나지 않는데도 프로그램은 끝나요. 아이템 세 개를 받고 나면 take(3)이 더 달라고 하지 않거든요.
내 컬렉션을 for 반복문에서 쓰기
섹션 제목: “내 컬렉션을 for 반복문에서 쓰기”지금까지는 구조체 자체가 반복자였어요. 그런데 실제로는 아이템을 담고 있는 구조체, 즉 컬렉션이 있고, 사람들이 Vec을 돌듯 그걸 돌 수 있게 하고 싶을 때가 많아요. 그럴 때는 IntoIterator를 구현해요. for 반복문이 뒤에서 쓰는 트레이트가 바로 이거예요. for x in thing은 사실 thing.into_iter()를 호출해요.
컬렉션은 보통 세 가지 방식으로 돌 수 있게 해 주는데, 내 타입에서 직접 보면 이해가 쉬워요.
| 이렇게 쓰면 | 뒤에서 호출되는 메서드 | 각 아이템의 타입 | 반복 후의 컬렉션 |
|---|---|---|---|
for s in &playlist |
playlist.iter() |
&String(읽기만 가능) |
계속 쓸 수 있어요 |
for s in &mut playlist |
playlist.iter_mut() |
&mut String(바꿀 수 있음) |
계속 쓸 수 있어요 |
for s in playlist |
playlist.into_iter() |
String(소유) |
이동되어 사라져요 |
세 가지를 모두 지원하는 Playlist를 만들어 볼게요. 이 구조체는 노래를 Vec에 담고 있으니까, 새 반복자를 직접 쓰는 대신 Vec의 반복자를 그대로 건네주면 돼요.
struct Playlist { songs: Vec<String>,}
impl Playlist { fn iter(&self) -> std::slice::Iter<'_, String> { self.songs.iter() }
fn iter_mut(&mut self) -> std::slice::IterMut<'_, String> { self.songs.iter_mut() }}
// `for song in &playlist`가 동작하게 해 줘요impl<'a> IntoIterator for &'a Playlist { type Item = &'a String; type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter { self.iter() }}
// `for song in &mut playlist`가 동작하게 해 줘요impl<'a> IntoIterator for &'a mut Playlist { type Item = &'a mut String; type IntoIter = std::slice::IterMut<'a, String>;
fn into_iter(self) -> Self::IntoIter { self.iter_mut() }}
// `for song in playlist`가 동작하게 해 줘요(그리고 playlist를 다 써 버려요)impl IntoIterator for Playlist { type Item = String; type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter { self.songs.into_iter() }}
fn main() { let mut playlist = Playlist { songs: vec![String::from("intro"), String::from("main theme"), String::from("outro")], };
for song in &playlist { println!("읽기: {}", song); }
for song in &mut playlist { *song = song.to_uppercase(); }
println!("가장 긴 곡: {:?}", playlist.iter().max_by_key(|s| s.len()));
for song in playlist { println!("가져가기: {}", song); } // 마지막 반복문이 `playlist`를 이동시켰으니, 여기서는 쓸 수 없어요.}실행 결과:
읽기: intro읽기: main theme읽기: outro가장 긴 곡: Some("MAIN THEME")가져가기: INTRO가져가기: MAIN THEME가져가기: OUTRO곡 제목은 영어 그대로 두었어요. to_uppercase()는 한글을 바꾸지 못하고, s.len()은 글자 수가 아니라 바이트 수를 세기 때문이에요(한글은 한 글자에 3바이트예요).
줄이 꽤 많지만, impl은 모두 같은 모양이에요. Item이 무엇인지 적고, 돌려줄 반복자 타입(IntoIter)을 적고, 그 반복자를 돌려주면 끝이에요. 'a는 라이프타임이에요. “빌려준 아이템은 빌려준 재생 목록만큼 살아 있어요”라는 뜻이에요. Vec을 감싸는 컬렉션이라면 거의 어디에나 이 패턴을 그대로 가져다 쓸 수 있어요.
거꾸로 돌기와 길이 알기
섹션 제목: “거꾸로 돌기와 길이 알기”알아 두면 좋은 트레이트가 두 개 더 있어요. 자주 필요하지는 않지만, .rev()나 .len() 같은 어댑터가 이것들에 기대고 있어요.
DoubleEndedIterator: 끝에서부터 아이템을 꺼내는next_back을 더해요. 이걸 구현하면.rev()를 쓸 수 있어요.ExactSizeIterator: 남은 아이템이 정확히 몇 개인지 안다고 약속해요. 이걸 구현하면.len()을 쓸 수 있어요.Iterator에서 정확한size_hint도 함께 제공해야 해요.
// low, low+1, ..., high-1을 내놓아요(범위 low..high처럼요)struct Between { low: u32, high: u32,}
impl Iterator for Between { type Item = u32;
fn next(&mut self) -> Option<u32> { if self.low >= self.high { return None; } let value = self.low; self.low += 1; Some(value) }
fn size_hint(&self) -> (usize, Option<usize>) { let left = self.high.saturating_sub(self.low) as usize; (left, Some(left)) }}
impl DoubleEndedIterator for Between { fn next_back(&mut self) -> Option<u32> { if self.low >= self.high { return None; } self.high -= 1; Some(self.high) }}
impl ExactSizeIterator for Between {}
fn main() { let backwards: Vec<u32> = Between { low: 1, high: 6 }.rev().collect(); println!("{:?}", backwards);
let mut both_ends = Between { low: 1, high: 6 }; println!("앞 {:?}, 뒤 {:?}", both_ends.next(), both_ends.next_back()); println!("{}개 남음", both_ends.len());}실행 결과:
[5, 4, 3, 2, 1]앞 Some(1), 뒤 Some(5)3개 남음양쪽 끝이 서로를 향해 다가오다가, 만나면 반복자가 끝나요. impl ExactSizeIterator for Between {}는 비어 있어요. len()은 이미 작성한 size_hint를 쓰거든요.
지름길: from_fn과 successors
섹션 제목: “지름길: from_fn과 successors”구조체와 impl을 쓰는 방식은 여러 번 다시 쓸 반복자에 좋아요. 한 번만 잠깐 쓸 반복자라면, 표준 라이브러리의 std::iter에 도우미 함수가 두 개 있어요.
iter::from_fn(closure): 값이 필요할 때마다 클로저를 호출해요. 클로저는next처럼Some(값)이나None을 돌려줘요.iter::successors(first, closure): 첫 번째 값에서 시작하고, 클로저가 이전 값으로부터 다음 값을 계산해요.None을 돌려주면 끝나요.
use std::iter;
fn main() { // from_fn: 클로저가 자기 상태(count)를 들고 있어요 let mut count = 3; let countdown: Vec<u32> = iter::from_fn(|| { if count == 0 { None } else { count -= 1; Some(count + 1) } }) .collect(); println!("{:?}", countdown);
// successors: 각 값을 바로 앞 값으로부터 계산해요 let powers: Vec<u32> = iter::successors(Some(1u32), |&n| n.checked_mul(10)).collect(); println!("{:?}", powers);
// 한 줄짜리 피보나치: 마지막 두 수를 쌍으로 들고 다녀요 let fib: Vec<u64> = iter::successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b))) .map(|(a, _)| a) .take(10) .collect(); println!("{:?}", fib);}실행 결과:
[3, 2, 1][1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000][0, 1, 1, 2, 3, 5, 8, 13, 21, 34]powers는 저절로 멈춰요. 다음 10의 거듭제곱인 10,000,000,000(100억)은 u32에 들어가지 않으니까, checked_mul이 None을 돌려주고 반복자가 끝나요. 좋은 기준이 하나 있어요. 함수 하나 안에서만 쓸 거라면 이 지름길을 쓰고, 다른 코드에서도 그 반복자를 만들어야 하거나 상태가 많다면 이름 있는 구조체를 만드세요.
자주 하는 실수
섹션 제목: “자주 하는 실수”next를 직접 호출하면서 mut를 빼먹기. next는 반복자를 바꾸니까, 변수가 가변이어야 해요.
let countdown = Countdown { current: 3 };println!("{:?}", countdown.next());error[E0596]: cannot borrow `countdown` as mutable, as it is not declared as mutable --> src/main.rs:20:22 |20 | println!("{:?}", countdown.next()); | ^^^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |19 | let mut countdown = Countdown { current: 3 }; | +++“countdown이 가변으로 선언되지 않았으니 가변으로 빌릴 수 없다”는 에러예요. mut를 붙이면 돼요. (for 반복문에는 필요 없어요. 반복문이 반복자를 가져가서 알아서 처리해 주거든요.)
type Item 빼먹기. 트레이트는 무엇을 건네주는지 알아야 해요.
error[E0046]: not all trait items implemented, missing: `Item` --> src/main.rs:5:1 |5 | impl Iterator for Countdown { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Item` in implementation | = help: implement the missing item: `type Item = /* Type */;`“트레이트 아이템 중 Item이 구현되지 않았다”는 뜻이에요. impl 안에 type Item = u32;(또는 반복자가 내놓는 타입)를 추가하세요.
끝나지 않는 반복자를 collect하기. take 없이 쓴 Fibonacci::new().collect::<Vec<_>>()는 컴파일은 잘 되지만, 쓸모 있는 방식으로 끝나지 않아요. 수가 오버플로될 때까지 돌거나, next가 절대 None을 돌려주지 않는다면 영원히 돌아요. 컴파일러는 이걸 잡아 주지 못해요. 끝나지 않을 수도 있는 반복자라면, 소비하기 전에 항상 take, take_while, find를 붙이세요.
type Item과fn next(&mut self) -> Option<Self::Item>으로Iterator를 구현해요. 끝나면None을 돌려줘요.next만 있으면map,filter,take,sum,zip을 비롯해 수십 가지 메서드가 공짜로 생겨요.- 반복자는 게을러요.
for반복문,collect,sum같은 것이 소비하기 전까지는 아무 일도 일어나지 않고, 아이템은 하나씩 흘러가요. &MyType,&mut MyType,MyType에IntoIterator를 구현하면, 빌리거나, 가변으로 빌리거나, 소비하는for반복문을 지원할 수 있어요.DoubleEndedIterator는.rev()를,ExactSizeIterator는.len()을 쓸 수 있게 해 줘요.- 한 번만 잠깐 쓸 반복자라면
std::iter::from_fn이나std::iter::successors를 쓰세요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. 반복자에 더 이상 아이템이 없을 때 next는 무엇을 돌려줘야 할까요?
2. next만 구현했을 때, 내 반복자에서 .map()과 .sum()을 호출할 수 있을까요?
3. 지연 평가 예제에서 next()가 처음 호출되는 건 언제일까요?
4. for song in &playlist가 동작하게 해 주는 구현은 무엇일까요?
5. 내 반복자에서 .rev()를 쓰려면 어떤 트레이트가 필요할까요?
점수: 0 / 5
더 알아보기
섹션 제목: “더 알아보기”- 반복자로 일련의 아이템들 처리하기 — The Rust Programming Language 한국어판:
Iterator와next가 어떻게 동작하는지, 반복자가 왜 게으른지 설명해요. std::iter모듈 (영어): 반복자에 대한 공식 안내서예요. 반복자를 직접 구현하는 법과IntoIterator에 대한 절도 있어요.Iterator트레이트 — 표준 라이브러리 문서 (영어):next만 쓰면 공짜로 얻는 어댑터가 모두 나와 있어요.- Iterators — Rust by Example (영어): 실행하고 고쳐 볼 수 있는 짧은 피보나치 반복자 예제예요.
std::iter::successors(영어): 시작 값과 “다음 값” 규칙으로 반복자를 만드는 함수의 문서예요.