반복자 기반 데이터 처리
Iterator의 next와 지연 평가를 이해하고 소비자·어댑터 메서드와 클로저를 조합해 컬렉션 처리를 구성합니다.
반복자 패턴은 일련의 원소에 차례로 어떤 작업을 하게 해 줍니다. 반복자는 각 원소를 차례로 돌고, 시퀀스가 언제 끝나는지 정하는 로직을 맡습니다. 반복자를 쓰면 이 로직을 직접 다시 구현하지 않아도 됩니다.
러스트에서 반복자는 게으릅니다(lazy). 반복자를 소비하는 메서드를 불러 반복자를 다 쓰기 전까지는 아무 일도 하지 않는다는 뜻입니다. 예를 들어 다음 코드는 Vec<T>에 정의된 iter 메서드를 불러 벡터 v1의 원소에 대한 반복자를 만듭니다. 이 코드만으로는 쓸모 있는 일을 하지 않습니다.
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();반복자는 v1_iter 변수에 저장됩니다. 반복자를 만든 뒤에는 여러 방법으로 쓸 수 있습니다. 제어 흐름문에서는 for 루프로 배열을 돌며 각 원소에 코드를 실행했습니다. 그때도 내부적으로는 반복자를 만들어 소비하고 있었지만, 지금까지는 정확히 어떻게 동작하는지 넘어갔습니다.
다음 코드는 반복자를 만드는 것과 for 루프에서 반복자를 쓰는 것을 나눈 예입니다. for 루프에서 v1_iter의 반복자를 쓰면, 반복자의 원소가 루프를 한 번 돌 때마다 하나씩 쓰이며 각 값을 출력합니다.
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}표준 라이브러리가 반복자를 제공하지 않는 언어에서는 같은 기능을 쓰려면, 변수를 인덱스 0에서 시작해 그 변수로 벡터를 인덱싱해 값을 얻고, 변수가 벡터의 원소 수에 이를 때까지 루프에서 그 값을 늘려 갈 것입니다. 반복자는 이 로직을 모두 대신 맡아, 실수할 수 있는 반복적인 코드를 줄여 줍니다. 반복자는 같은 로직을 벡터처럼 인덱싱할 수 있는 자료 구조뿐 아니라 여러 종류의 시퀀스에 쓸 수 있는 유연함도 줍니다. 반복자가 어떻게 그렇게 하는지 살펴봅니다.
Iterator 트레이트와 next 메서드
모든 반복자는 표준 라이브러리에 정의된 Iterator라는 트레이트를 구현합니다. 트레이트의 정의는 다음과 같습니다.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 기본 구현이 있는 메서드는 생략
}이 정의에는 새 문법 type Item과 Self::Item이 쓰였습니다. 이것은 이 트레이트의 연관 타입(associated type)을 정의하는 것입니다. 연관 타입은 고급 트레이트에서 자세히 다룹니다. 지금은 이 코드가 Iterator 트레이트를 구현하려면 Item 타입도 정의해야 하며, 이 Item 타입이 next 메서드의 반환 타입에 쓰인다는 것만 알면 됩니다. 다시 말해 Item 타입은 반복자가 돌려주는 값의 타입입니다.
Iterator 트레이트를 구현하는 쪽이 정의해야 하는 메서드는 next 하나뿐입니다. next는 Some으로 감싼 반복자의 원소를 한 번에 하나씩 돌려주고, 반복이 끝나면 None을 돌려줍니다. 반복자의 next 메서드를 직접 부를 수도 있습니다. 다음 테스트는 벡터로 만든 반복자에 next를 여러 번 부르면 어떤 값이 나오는지 보여 줍니다.
#[cfg(test)]
mod tests {
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
}v1_iter를 가변으로 만들어야 했다는 점을 보세요. 반복자에 next를 부르면, 반복자가 시퀀스의 어디까지 왔는지 기록하는 내부 상태가 바뀝니다. 곧 이 코드는 반복자를 소비하거나 다 써 버립니다. next를 부를 때마다 반복자의 원소 하나를 먹습니다. for 루프를 쓸 때는 v1_iter를 가변으로 만들 필요가 없었는데, 루프가 v1_iter의 소유권을 가져가 내부적으로 가변으로 만들기 때문입니다. 아래 그림은 같은 반복자에 next를 네 번 부를 때 무엇이 바뀌는지 보여 줍니다.
반복자 소스·상태·Item 소유권
Vec[1,2,3] iter의 네 next에서 Some&1/&2/&3/None으로 반복자 상태가 진행한다. 원본 소유 이동과 일반 Iterator의 None 이후 보장은 별도로 유지한다.
RUST · ITERATOR STATE & ITEMS
next는 반복자 상태를 진행시키고 Item을 반환한다
본문 Vec [1, 2, 3]의 iter()는 각 값을 빌립니다. 반복자 자체를 가변으로 두는 이유와 원소의 소유권을 별도로 읽습니다.
본문의 next 결과
Some(&1) → Some(&2) → Some(&3) → None. 위치는 반복자에 저장되며 원본 Vec를 빌립니다.
iter() · 공유 빌림
Vec<T>를 공유로 빌리고 Item = &T를 냅니다. 위 예제의 v1은 보존됩니다.
iter_mut() · 가변 빌림
Vec<T>를 가변으로 빌리고 Item = &mut T를 냅니다. 반복자를 진행시키는 가변 접근과 원소를 바꾸는 접근은 별개입니다.
소유 Vec의 into_iter()
Vec<T>를 소유하여 Item = T를 냅니다. IntoIterator는 참조에도 구현되므로 into_iter라는 이름만으로 항상 소유 값이라고 판단하지 않습니다.
next(&mut self) · 종료 계약
fn next(&mut self) -> Option<Self::Item>;일반 Iterator는 None 뒤에 다시 Some을 낼 수도 있습니다. 계속 None이라는 보장은 FusedIterator 또는 fuse()로 표현합니다.
for는 IntoIterator를 사용합니다. 반복자 생성은 원소 순회를 시작하지 않으며, next·for·소비자가 실제 진행을 요청합니다. sum은 본문 [1, 2, 3]을 소비해 6을 얻습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | next를 순서대로 호출하면 Some 공유 참조 1, 2, 3 이후 None을 얻는다. 원본 Vec은 이동되지 않는다. |
| 관계·조건 설명 | 본문의 Vec [1,2,3]에 대한 공유 iter에서 같은 반복자로 next를 네 번 호출한다. 상태 변화는 네 노드와 세 시간 간선으로 나타내며, Some으로 참조 1,2,3을 반환한 뒤 None을 반환한다. 반복자 위치만 진행하며 원본 Vec의 소유권이나 숫자를 바꾸지 않는다. 이 slice 반복자의 종료 결과와 일반 Iterator에서 None 뒤에 Some이 가능한 계약은 아래 보조 설명으로 구분한다. |
| 본문·주석 | Some(&1) → Some(&2) → Some(&3) → None. 위치는 반복자에 저장되며 원본 Vec을 빌립니다. |
| 본문·주석 | for는 IntoIterator를 사용합니다. 반복자 생성은 원소 순회를 시작하지 않으며, next·for·소비자가 실제 진행을 요청합니다. sum은 본문 반복자의 값 [1, 2, 3]을 소비해 6을 얻습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 공유 반복자 값 [1,2,3]을 가진 Vec의 공유 반복자 iter를 만든다. |
|
|
| next 진행 요청마다 다음 요소 참조가 나온다. |
|
|
| 소진 이 Vec 반복자의 네 번째 next를 확인한다. |
|
|
반복자 소스·상태·Item 소유권
Vec[1,2,3] iter의 네 next에서 Some&1/&2/&3/None으로 반복자 상태가 진행한다. 원본 소유 이동과 일반 Iterator의 None 이후 보장은 별도로 유지한다.
RUST · ITERATOR STATE & ITEMS
next는 반복자 상태를 진행시키고 Item을 반환한다
본문 Vec [1, 2, 3]의 iter()는 각 값을 빌립니다. 반복자 자체를 가변으로 두는 이유와 원소의 소유권을 별도로 읽습니다.
본문의 next 결과
Some(&1) → Some(&2) → Some(&3) → None. 위치는 반복자에 저장되며 원본 Vec를 빌립니다.
iter() · 공유 빌림
Vec<T>를 공유로 빌리고 Item = &T를 냅니다. 위 예제의 v1은 보존됩니다.
iter_mut() · 가변 빌림
Vec<T>를 가변으로 빌리고 Item = &mut T를 냅니다. 반복자를 진행시키는 가변 접근과 원소를 바꾸는 접근은 별개입니다.
소유 Vec의 into_iter()
Vec<T>를 소유하여 Item = T를 냅니다. IntoIterator는 참조에도 구현되므로 into_iter라는 이름만으로 항상 소유 값이라고 판단하지 않습니다.
next(&mut self) · 종료 계약
fn next(&mut self) -> Option<Self::Item>;일반 Iterator는 None 뒤에 다시 Some을 낼 수도 있습니다. 계속 None이라는 보장은 FusedIterator 또는 fuse()로 표현합니다.
for는 IntoIterator를 사용합니다. 반복자 생성은 원소 순회를 시작하지 않으며, next·for·소비자가 실제 진행을 요청합니다. sum은 본문 [1, 2, 3]을 소비해 6을 얻습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | next를 순서대로 호출하면 Some 공유 참조 1, 2, 3 이후 None을 얻는다. 원본 Vec은 이동되지 않는다. |
| 관계·조건 설명 | 본문의 Vec [1,2,3]에 대한 공유 iter에서 같은 반복자로 next를 네 번 호출한다. 상태 변화는 네 노드와 세 시간 간선으로 나타내며, Some으로 참조 1,2,3을 반환한 뒤 None을 반환한다. 반복자 위치만 진행하며 원본 Vec의 소유권이나 숫자를 바꾸지 않는다. 이 slice 반복자의 종료 결과와 일반 Iterator에서 None 뒤에 Some이 가능한 계약은 아래 보조 설명으로 구분한다. |
| 본문·주석 | Some(&1) → Some(&2) → Some(&3) → None. 위치는 반복자에 저장되며 원본 Vec을 빌립니다. |
| 본문·주석 | for는 IntoIterator를 사용합니다. 반복자 생성은 원소 순회를 시작하지 않으며, next·for·소비자가 실제 진행을 요청합니다. sum은 본문 반복자의 값 [1, 2, 3]을 소비해 6을 얻습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 공유 반복자 값 [1,2,3]을 가진 Vec의 공유 반복자 iter를 만든다. |
|
|
| next 진행 요청마다 다음 요소 참조가 나온다. |
|
|
| 소진 이 Vec 반복자의 네 번째 next를 확인한다. |
|
|
next를 부를 때마다 반복자에 저장된 위치가 0에서 1로, 1에서 2로 나아가며 Some(&1), Some(&2), Some(&3)을 돌려주고, 네 번째에는 None을 돌려줍니다. 진행하는 것은 반복자의 위치뿐이며, 원본 벡터의 소유권이나 값은 바뀌지 않습니다. next가 &mut self를 받는 것은 이 위치를 바꾸기 때문입니다. 그림이 밝히듯 일반적인 Iterator는 None을 돌려준 뒤에도 다시 Some을 돌려줄 수 있으며, 그 뒤로 계속 None이라는 보장이 필요하면 FusedIterator를 구현한 반복자를 쓰거나 fuse()를 부릅니다. 반복자를 만드는 것만으로는 순회가 시작되지 않고, next나 for, 소비하는 메서드가 진행을 요청해야 합니다.
next를 불러 얻는 값은 벡터 안 값의 불변 참조라는 점도 보세요. iter 메서드는 불변 참조를 내놓는 반복자를 만듭니다. v1의 소유권을 가져가 소유한 값을 돌려주는 반복자를 만들고 싶다면 iter 대신 into_iter를 부릅니다. 마찬가지로 가변 참조를 돌며 값을 바꾸고 싶다면 iter 대신 iter_mut를 부릅니다.
| 메서드 | 벡터를 다루는 방식 | Item |
|---|---|---|
iter() | 공유로 빌림 | &T |
iter_mut() | 가변으로 빌림 | &mut T |
into_iter() | 소유권을 가져감 | T |
반복자를 진행시키는 가변 접근과 원소를 바꾸는 접근은 별개입니다. 또 IntoIterator는 &Vec<T> 같은 참조에도 구현되어 있으므로, into_iter라는 이름만 보고 언제나 소유한 값이 나온다고 판단하면 안 됩니다. for 루프는 이 IntoIterator 트레이트로 반복자를 얻습니다.
더 깊이: for는 무엇으로 바뀌고, 배열의 into_iter는 왜 에디션마다 다를까
Reference는 for 패턴 in 식 { 본문 }이 어떤 코드와 같은지 정해 둡니다. 먼저 IntoIterator::into_iter(식)으로 반복자를 하나 얻어 가변 변수에 담고, loop 안에서 Iterator::next(&mut iter)를 불러 Some(val)이면 그 값을 패턴에 묶어 본문을 실행하며, None이면 break합니다. 그래서 for에 넘긴 값은 into_iter로 소비되고, for x in &v가 참조를 내놓는 것은 &Vec<T>의 IntoIterator 구현이 그렇게 정해 두었기 때문입니다. for가 받은 반복자를 자기만의 가변 바인딩에 옮겨 담은 뒤 next를 부르므로, 앞에서 본 것처럼 for에 넘기는 반복자 변수를 mut로 만들 필요가 없습니다.
배열은 Rust 1.53부터 값으로 반복하는 IntoIterator를 구현합니다. 그런데 그 전에는 array.into_iter()라고 쓰면 자동 참조로 (&array).into_iter()가 불려 참조를 내놓았으므로, 이 뜻에 기대는 코드가 많았습니다. 에디션 가이드에 따르면 그래서 2015와 2018 에디션에서는 메서드 호출 문법의 array.into_iter()만 예전처럼 참조를 내놓도록 남겨 두었고, 2021 에디션부터는 값을 내놓습니다. for e in [1, 2, 3]처럼 메서드 호출 문법을 쓰지 않는 경우는 모든 에디션에서 값으로 반복하며, 에디션과 상관없이 참조를 원한다면 iter()를 씁니다.
반복자를 소비하는 메서드
Iterator 트레이트에는 표준 라이브러리가 기본 구현을 제공하는 여러 메서드가 있습니다. Iterator 트레이트의 표준 라이브러리 API 문서에서 이 메서드들을 볼 수 있습니다. 이 메서드들 가운데 일부는 정의 안에서 next 메서드를 부르며, 그래서 Iterator 트레이트를 구현할 때 next 메서드를 꼭 구현해야 합니다.
next를 부르는 메서드를 소비 어댑터(consuming adapter)라고 부릅니다. 부르면 반복자를 다 써 버리기 때문입니다. 한 예가 sum 메서드로, 반복자의 소유권을 가져가 next를 되풀이해 부르며 원소를 돌고, 그동안 반복자를 소비합니다. 원소를 돌면서 각 원소를 누적값에 더하고, 반복이 끝나면 합계를 돌려줍니다. 다음 테스트는 sum 메서드의 쓰임을 보여 줍니다.
#[cfg(test)]
mod tests {
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
}sum을 부른 뒤에는 v1_iter를 쓸 수 없습니다. sum이 자신을 부른 반복자의 소유권을 가져가기 때문입니다.
다른 반복자를 생성하는 메서드
반복자 어댑터(iterator adapter)는 Iterator 트레이트에 정의된 메서드로, 반복자를 소비하지 않습니다. 대신 원래 반복자의 어떤 면을 바꾼 다른 반복자를 만들어 냅니다. 다음 코드는 반복자 어댑터 메서드 map을 부르는 예로, map은 원소를 돌 때 각 원소에 부를 클로저를 받습니다. map 메서드는 원소를 바꾼 새 반복자를 돌려줍니다. 여기서 클로저는 벡터의 각 원소에 1을 더한 새 반복자를 만듭니다.
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
}하지만 이 코드는 경고를 냅니다. 경고는 컴파일을 막지 않으므로 프로그램은 빌드되고 실행되며, 경고의 대상은 map이 돌려준 Map 값을 아무도 쓰지 않는다는 점입니다.
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++이 코드는 아무 일도 하지 않습니다. 지정한 클로저는 한 번도 불리지 않습니다. 경고가 그 까닭을 일깨워 줍니다. 반복자 어댑터는 게으르므로, 여기서 반복자를 소비해야 합니다. 이 경고를 고치고 반복자를 소비하기 위해 collect 메서드를 씁니다. 이 메서드는 커맨드 라인 인수 받기에서 env::args와 함께 썼습니다. 이 메서드는 반복자를 소비하고, 결과로 나온 값들을 컬렉션 데이터 타입으로 모읍니다.
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
}map은 클로저를 받으므로, 각 원소에 하고 싶은 어떤 연산이든 지정할 수 있습니다. 이것은 Iterator 트레이트가 제공하는 반복 동작을 재사용하면서, 클로저로 일부 동작을 바꿔 끼우는 좋은 예입니다. 반복자 어댑터를 여러 번 이어 부르면 복잡한 동작을 읽기 쉬운 방식으로 할 수 있습니다. 다만 모든 반복자가 게으르므로, 반복자 어댑터를 부른 결과를 얻으려면 소비 어댑터 메서드 가운데 하나를 불러야 합니다. 아래 그림은 이 글의 반복자 체인 예제들에서 원소가 어떻게 처리되는지 보여 줍니다.
소비자가 당겨 실행하는 지연 반복자 체인
체인 생성 때 미처리, collect의 뒤→앞 next 요청에 앞→뒤 원소/ +1 결과가 전달되어 [2,3,4]에 누적되는 실제 지연 평가이다.
RUST · LAZY PULL CHAIN
체인을 만드는 일과 원소를 평가하는 일을 구분한다
본문의 map 예제는 collect에서 결과를 모읍니다. Shoe 필터 예제는 별개의 체인이며 map을 거치지 않습니다.
map 체인 · 요청과 결과
collect → map → iter 순으로 다음 원소를 요청하고 iter → map → collect로 결과를 돌려줍니다. &1·&2·&3이 2·3·4가 되어 새 Vec에 모입니다.
생성만 한 경우 · 예제 12-14
v1.iter().map(|x| x + 1);Map 값은 만들어지지만 여기서는 next를 요청하지 않습니다. 클로저는 실행되지 않으며 사용하지 않은 지연 반복자 경고가 납니다.
소비한 경우 · 예제 12-15
let v2: Vec<_> = v1.iter()
.map(|x| x + 1).collect();map은 Item을 값으로 받습니다. 여기의 Item은 &i32이고, 최종 Vec는 새 i32 값을 보관합니다.
별도의 filter · 예제 12-16
shoes.into_iter()
.filter(|s| s.size == shoe_size)
.collect()Item은 소유 Shoe지만 predicate는 &Shoe를 빌립니다. size 10 sneaker·13 sandal·10 boot에서 shoe_size 10이면 sneaker와 boot가 남습니다.
지연성과 할당의 경계
map·filter는 self를 받아 이전 반복자를 어댑터 안에 저장합니다. 생성 시 원소를 전부 처리하지 않는다는 뜻입니다. find·take 등은 일찍 멈출 수 있고 collect나 클로저 본문은 할당할 수 있습니다.
단순 어댑터 체인은 중간 Vec 없이 작동할 수 있지만, 지연성이 전체 작업의 무할당이나 성능 우위를 보장하지는 않습니다. filter는 &Item을 검사한 뒤 통과한 Item을 그대로 내보냅니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 세 노드와 네 관계다. 실선의 원소 전달은 iter→map→collect이며 참조 값에 1을 더한 새 i32가 Vec에 저장된다. 점선 요청은 collect→map과 map→iter로 반대 방향이다. 두 요청은 별도 외부 레일로 중간 노드 내부를 통과하지 않는다. 정의만 할 때는 원소 처리가 없고 소비자가 next를 요청할 때 진행한다. Shoe의 into_iter/filter는 아래 별도 사례이며 map 노드를 거치는 흐름이 아니다. |
| 본문·주석 | map은 Item을 값으로 받습니다. 여기의 Item은 &i32이고, 최종 Vec은 새 i32 값을 보관합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 체인 생성 iter·map 체인을 만들어도 아직 요소를 처리하지 않는다. |
|
|
| collect 요청 소비자가 뒤에서 next를 요청해 원본까지 전파한다. |
|
|
| 결과 누적 각 원소에 1을 더해 새 결과를 모은다. |
|
|
소비자가 당겨 실행하는 지연 반복자 체인
체인 생성 때 미처리, collect의 뒤→앞 next 요청에 앞→뒤 원소/ +1 결과가 전달되어 [2,3,4]에 누적되는 실제 지연 평가이다.
RUST · LAZY PULL CHAIN
체인을 만드는 일과 원소를 평가하는 일을 구분한다
본문의 map 예제는 collect에서 결과를 모읍니다. Shoe 필터 예제는 별개의 체인이며 map을 거치지 않습니다.
map 체인 · 요청과 결과
collect → map → iter 순으로 다음 원소를 요청하고 iter → map → collect로 결과를 돌려줍니다. &1·&2·&3이 2·3·4가 되어 새 Vec에 모입니다.
생성만 한 경우 · 예제 12-14
v1.iter().map(|x| x + 1);Map 값은 만들어지지만 여기서는 next를 요청하지 않습니다. 클로저는 실행되지 않으며 사용하지 않은 지연 반복자 경고가 납니다.
소비한 경우 · 예제 12-15
let v2: Vec<_> = v1.iter()
.map(|x| x + 1).collect();map은 Item을 값으로 받습니다. 여기의 Item은 &i32이고, 최종 Vec는 새 i32 값을 보관합니다.
별도의 filter · 예제 12-16
shoes.into_iter()
.filter(|s| s.size == shoe_size)
.collect()Item은 소유 Shoe지만 predicate는 &Shoe를 빌립니다. size 10 sneaker·13 sandal·10 boot에서 shoe_size 10이면 sneaker와 boot가 남습니다.
지연성과 할당의 경계
map·filter는 self를 받아 이전 반복자를 어댑터 안에 저장합니다. 생성 시 원소를 전부 처리하지 않는다는 뜻입니다. find·take 등은 일찍 멈출 수 있고 collect나 클로저 본문은 할당할 수 있습니다.
단순 어댑터 체인은 중간 Vec 없이 작동할 수 있지만, 지연성이 전체 작업의 무할당이나 성능 우위를 보장하지는 않습니다. filter는 &Item을 검사한 뒤 통과한 Item을 그대로 내보냅니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 세 노드와 네 관계다. 실선의 원소 전달은 iter→map→collect이며 참조 값에 1을 더한 새 i32가 Vec에 저장된다. 점선 요청은 collect→map과 map→iter로 반대 방향이다. 두 요청은 별도 외부 레일로 중간 노드 내부를 통과하지 않는다. 정의만 할 때는 원소 처리가 없고 소비자가 next를 요청할 때 진행한다. Shoe의 into_iter/filter는 아래 별도 사례이며 map 노드를 거치는 흐름이 아니다. |
| 본문·주석 | map은 Item을 값으로 받습니다. 여기의 Item은 &i32이고, 최종 Vec은 새 i32 값을 보관합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 체인 생성 iter·map 체인을 만들어도 아직 요소를 처리하지 않는다. |
|
|
| collect 요청 소비자가 뒤에서 next를 요청해 원본까지 전파한다. |
|
|
| 결과 누적 각 원소에 1을 더해 새 결과를 모은다. |
|
|
그림에서 예제 12-14, 12-15, 12-16이라고 적은 칸은 차례로 이 글에서 map만 만든 코드, collect까지 부른 코드, 다음 절에서 볼 filter 코드입니다. v1.iter().map(|x| x + 1);은 Map 값을 만들기만 하고 next를 요청하지 않으므로 클로저가 실행되지 않습니다. collect를 부르면 collect가 map에, map이 iter에 다음 원소를 요청하고, 결과는 반대 방향으로 돌아옵니다. iter가 내놓은 &1, &2, &3이 map에서 1이 더해져 2, 3, 4가 되고 새 Vec에 모입니다. map의 클로저는 Item을 값으로 받으며, 여기서 Item은 &i32이고 결과 벡터에는 새 i32 값이 들어갑니다.
map과 filter는 self를 받아 이전 반복자를 어댑터 안에 저장할 뿐, 만들 때 원소를 처리하지 않습니다. find나 take 같은 메서드는 원소를 끝까지 보지 않고 일찍 멈출 수 있습니다. 단순한 어댑터 체인은 중간 Vec 없이 동작할 수 있지만, collect나 클로저 본문은 메모리를 할당할 수 있으므로, 게으르다는 것이 할당이 없다거나 언제나 빠르다는 보장은 아닙니다.
환경을 캡처하는 클로저 사용하기
많은 반복자 어댑터는 클로저를 인수로 받으며, 반복자 어댑터에 넘기는 클로저는 흔히 자신의 환경을 캡처합니다. 예로 클로저를 받는 filter 메서드를 씁니다. 이 클로저는 반복자의 원소를 받아 bool을 돌려줍니다. 클로저가 true를 돌려주면 그 값이 filter가 만드는 반복자에 들어가고, false를 돌려주면 들어가지 않습니다.
다음 코드는 환경에서 shoe_size 변수를 캡처하는 클로저와 함께 filter를 써서 Shoe 구조체 인스턴스의 컬렉션을 돕니다. 지정한 크기의 신발만 돌려줍니다.
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];
let in_my_size = shoes_in_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}shoes_in_size 함수는 신발 벡터의 소유권과 신발 크기를 매개변수로 받아, 지정한 크기의 신발만 담은 벡터를 돌려줍니다. shoes_in_size의 본문에서는 into_iter를 불러 벡터의 소유권을 가져가는 반복자를 만듭니다. 그다음 filter를 불러, 클로저가 true를 돌려주는 원소만 담은 새 반복자로 바꿉니다.
클로저는 환경에서 shoe_size 매개변수를 캡처하고 그 값을 각 신발의 크기와 비교해, 지정한 크기의 신발만 남깁니다. 마지막으로 collect를 불러, 바뀐 반복자가 돌려주는 값들을 함수가 돌려줄 벡터로 모읍니다. 그림에서처럼 이 체인의 Item은 소유한 Shoe이지만, filter의 클로저는 &Shoe를 빌려 검사한 뒤 통과한 Shoe를 그대로 내보냅니다. 테스트는 shoes_in_size를 부르면 지정한 값과 크기가 같은 신발만 돌려받는다는 것을 보여 줍니다. 크기가 10인 sneaker와 boot가 남고 13인 sandal은 빠집니다.
정리
- 러스트의 반복자는 게으르며,
next나for, 소비하는 메서드가 요청해야 원소를 처리합니다. Iterator트레이트를 구현하려면 연관 타입Item과fn next(&mut self) -> Option<Self::Item>만 정의하면 됩니다.next는 반복자의 위치를 바꾸므로 반복자를 가변으로 두어야 합니다.iter()는&T를,iter_mut()는&mut T를, 소유한Vec<T>의into_iter()는T를 내놓습니다.sum이나collect같은 소비 어댑터는 반복자를 다 쓰고,map이나filter같은 반복자 어댑터는 새 반복자를 만들 뿐이므로 소비하지 않으면 경고가 납니다.- 반복자 어댑터에 넘기는 클로저는
shoe_size처럼 환경의 값을 캡처할 수 있습니다.
연습 문제
1. 무엇이 출력될까
다음 코드는 무엇을 출력하나요? 마지막 줄의 collect를 지우면 어떻게 되나요?
let v = vec![1, 2, 3, 4];
let doubled: Vec<i32> = v
.iter()
.map(|x| {
println!("map {x}");
x * 2
})
.filter(|x| *x > 4)
.collect();
println!("{doubled:?}");해설 보기
collect가 원소를 하나씩 당겨 오므로 map 1, map 2, map 3, map 4가 차례로 출력되고, 두 배 한 값 2, 4, 6, 8 가운데 4보다 큰 것만 남아 마지막에 [6, 8]이 출력됩니다. collect를 지우면 Vec<i32> 자리에 Filter 반복자를 넣으려 하므로 타입 오류로 컴파일되지 않습니다. 타입 표기와 마지막 출력까지 지워 반복자를 만들어 두기만 하면 컴파일은 되지만, 소비하지 않으므로 클로저가 한 번도 불리지 않아 map 줄이 하나도 출력되지 않습니다.
2. 어느 메서드를 쓸까
(가) 벡터의 모든 점수에 보너스 5점을 더해 원래 벡터를 바꾸고 싶다. (나) 벡터를 더는 쓰지 않으니 각 String을 대문자로 바꾼 새 벡터를 만들고 싶다. 각각 iter, iter_mut, into_iter 가운데 무엇이 알맞나요?
해설 보기
(가)는 원래 벡터의 원소를 바꾸어야 하므로 iter_mut()로 &mut i32를 얻어 for score in scores.iter_mut() { *score += 5; }처럼 씁니다. (나)는 원래 벡터가 더는 필요 없으므로 into_iter()로 String의 소유권을 받아 map(|s| s.to_uppercase())로 바꾼 뒤 collect하면 됩니다. 원래 벡터를 그대로 두고 새 벡터만 만들고 싶다면 iter()를 씁니다.
다음 글 I/O 프로젝트 개선에서는 반복자로 11장의 minigrep에서 불필요한 clone과 인덱싱을 없애고 검색 코드를 간결하게 고칩니다.