반복자 기반 데이터 처리
Iterator의 next와 지연 평가를 이해하고 소비자·어댑터 메서드와 클로저를 조합해 컬렉션 처리를 구성합니다.
반복자 패턴은 일련의 아이템들에 대해 순서대로 어떤 작업을 수행할 수 있도록 해줍니다.
반복자는 각 아이템을 순회하고 언제 시퀀스가 종료될지 결정하는 로직을 담당합니다.
반복자를 사용하면, 그런 로직을 다시 구현할 필요가 없습니다.
러스트에서의 반복자는 게으른데, 이는 반복자를 사용하는 메서드를 호출하여 반복자를 소비하기 전까지는 동작을 하지 않는다는 의미입니다.
예를 들면, 예제 12-10의 코드는 Vec<T>에 정의된 iter 메서드를 호출함으로써 벡터 v1에 있는 아이템들에 대한 반복자를 생성합니다.
이 코드 자체로는 어떤 유용한 동작도 하지 않습니다.
반복자 소스·상태·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를 확인한다. |
|
|
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
반복자는 v1_iter 변수에 저장됩니다.
일단 반복자를 만들면, 다양한 방법으로 사용할 수 있습니다.
2장의 예제 2-5에서는 각 아이템에 대해 어떤 코드를 실행하기 위해 for 루프를 사용하여 어떤 배열에 대한 반복을 수행했습니다.
내부적으로는 암묵적으로 반복자를 생성한 다음 소비하는 것이었지만, 지금까지는 이게 정확히 어떻게 동작하는지에 대해서 대충 넘겼습니다.
예제 12-11의 예제에서는 for 루프에서 반복자를 사용하는 부분으로부터 반복자 생성을 분리했습니다.
v1_iter에 있는 반복자를 사용하여 for 루프가 호출되면, 반복자의 각 요소가 루프의 한 순번마다 사용되는데, 여기서는 각각의 값을 출력합니다.
for 루프에서 반복자 사용하기
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)을 정의합니다.
연관 타입에 대해서는 18장에서 더 자세히 이야기하겠습니다.
현재로서는 이 코드에서 Iterator 트레이트를 구현하려면 Item 타입도 함께 정의되어야 하며, 이 Item 타입이 next 메서드의 반환 타입으로 사용된다는 것만 알면 됩니다.
바꿔 말하면, Item 타입은 반복자로부터 반환되는 타입이 되겠습니다.
Iterator 트레이트는 구현하려는 이에게 딱 하나의 메서드 정의를 요구합니다.
바로 next 메서드인데, 이 메서드는 Some으로 감싼 반복자의 아이템을 하나씩 반환하고, 반복자가 종료될 때는 None을 반환합니다.
반복자의 next 메서드를 직접 호출할 수 있습니다; 예제 12-12는
벡터로부터 생성된 반복자에 대하여 next를 반복적으로 호출했을 때 어떤
값들이 반환되는지 보여줍니다.
next 메서드
호출하기
#[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 메서드 호출은 반복자 내부의 상태를 변경하여 반복자가 현재 시퀀스의 어디에 있는지 추적합니다.
바꿔 말하면, 이 코드는 반복자를 소비(consume), 즉 다 써 버립니다.
next에 대한 각 호출은 반복자로부터 하나의 아이템을 소비합니다.
for 루프를 사용할 때는 v1_iter를 가변으로 만들 필요가 없는데, 루프가 v1_iter의 소유권을 갖고 내부적으로 가변으로 만들기 때문입니다.
또한 next 호출로 얻어온 값들은 벡터 내의 값들에 대한 불변 참조자라는 점도 주의하세요.
iter 메서드는 불변 참조자에 대한 반복자를 생성합니다.
만약 v1의 소유권을 얻어서 소유한 값을 반환하도록 하고 싶다면, iter 대신 into_iter를 호출할 수 있습니다.
비슷하게, 가변 참조자에 대한 반복자가 필요하면, iter 대신 iter_mut을 호출할 수 있습니다.
반복자를 소비하는 메서드
Iterator 트레이트에는 표준 라이브러리에서 기본 구현을 제공하는 여러 가지 메서드가 있습니다; 이 메서드들은 표준 라이브러리 API 문서의 Iterator 트레이트에 대한 부분을 살펴보면 찾을 수 있습니다.
이 메서드들 중 일부는 정의 부분에서 next 메서드를 호출하는데, 이것이 Iterator 트레이트를 구현할 때 next 메서드를 구현해야만 하는 이유입니다.
next를 호출하는 메서드들을 소비 어댑터(consuming adaptor)라고 하는데, 호출하면 반복자를 소비하기 때문에 그렇습니다.
한 가지 예로 sum 메서드가 있는데, 이는 반복자의 소유권을 가져온 다음 반복적으로 next를 호출하는 방식으로 순회하며, 따라서 반복자를 소비합니다.
전체를 순회하면서 현재의 합계값에 각 아이템을 더하고 순회가 완료되면 합계를 반환합니다.
예제 12-13은 sum 메서드 사용 방식을 보여주는 테스트입니다.
sum 메서드를 호출하여 반복자의
모든 아이템에 대한 합계 구하기
#[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은 반복자를 소유하여 호출하므로, sum을 호출한 이후에는 v1_iter의
사용이 허용되지 않습니다.
다른 반복자를 생성하는 메서드
소비자가 당겨 실행하는 지연 반복자 체인
체인 생성 때 미처리, 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을 더해 새 결과를 모은다. |
|
|
반복자 어댑터(iterator adaptor)는 새 반복자를 만드는 Iterator 메서드입니다. map과 filter는 기존 반복자를 값으로 받아 어댑터 안에 저장하지만, 생성하는 것만으로 원소를 모두 순회하지는 않습니다.
대신 원본 반복자의 어떤 측면을 바꿔서 다른 반복자를 제공합니다.
예제 12-14는 반복자 어댑터 메서드인 map을 호출하는 예를 보여주는데, 클로저를 인수로 받아 새 반복자에 저장합니다. 이후 next 요청이 전달될 때 필요한 아이템에 클로저를 적용합니다.
map 메서드는 수정된 아이템들을 생성하는 새로운 반복자를 반환합니다.
여기에서는 클로저가 벡터의 각 아이템에 1을 더하고, 새로운 반복자가 그 결과를 반환합니다.
예제 12-14: 반복자 어댑터map을 호출하여
새로운 반복자 생성하기
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
하지만 이 코드는 다음과 같은 경고를 발생시킵니다.
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
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)]` on by default
warning: `iterators` (bin "iterators") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
예제 12-14의 코드는 아무것도 하지 않습니다; 넘겨진 클로저는 결코 호출되지 않습니다.
위 경고는 이유가 무엇인지 상기시켜 줍니다.
반복자 어댑터는 게으르고, 반복자를 여기서 소비할 필요가 있다는 것을요.
이 경고를 수정하고 반복자를 소비하기 위해서 collect 메서드를 사용할 것인데, 11장의 예제 11-1에서 env::args와 함께 사용했었지요.
이 메서드는 반복자를 소비하고 결괏값을 모아서 컬렉션 데이터 타입으로 만들어 줍니다.
예제 12-15에서는 벡터에 map을 호출하여 얻은 반복자를 순회하면서 결과를 모읍니다.
이 벡터는 원본 벡터로부터 1씩 증가한 아이템들을 담고 있는 상태가 될 것입니다.
예제 12-15:map을 호출하여 새로운 반복자를
생성한 다음 collect 메서드를 호출하여 이 반복자를 소비하고 새로운 벡터
생성하기
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 트레이트가 제공하는 반복 동작을 재사용하면서 클로저로 동작의 일부를 커스터마이징할 수 있게 해주는 방법을 보여주는 훌륭한 예입니다.
반복자 어댑터의 호출을 연결시키면 복잡한 동작을 읽기 쉬운 방식으로 수행할 수 있습니다.
하지만 모든 반복자는 게으르므로, 반복자 어댑터를 호출한 결과를 얻기 위해서는 소비 어댑터 중 하나를 호출해야만 합니다.
환경을 캡처하는 클로저 사용하기
많은 반복자 어댑터는 클로저를 인수로 사용하고, 보통 반복자 어댑터의 인수에 명시되는 클로저는 자신의 환경을 캡처하는 클로저일 것입니다.
이러한 예를 들기 위해 클로저 인수를 사용하는 filter 메서드를 사용해보겠습니다.
이 클로저는 반복자로부터 아이템을 받아서 bool을 반환합니다.
만일 클로저가 true를 반환하면, 그 값을 filter에 의해 생성된 반복자에 포함시키게 됩니다.
클로저가 false를 반환하면 해당 값은 포함시키지 않습니다.
예제 12-16에서는 환경으로부터 shoe_size를 캡처하는 클로저를 가지고 filter를 사용하여 Shoe 구조체 인스턴스의 컬렉션을 순회합니다.
이는 지정된 크기의 신발만 반환해 줄 것입니다.
예제 12-16:shoe_size를 캡처하는 클로저로
filter 메서드 사용하기
#[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를 호출하면 적용된 반복자에 의해 반환된 값을 벡터로 모으고, 이 벡터가 함수에 의해 반환됩니다.
이 테스트는 shoes_in_size를 호출했을 때 지정된 값과 동일한 크기인
신발들만 돌려받는다는 것을 보여 줍니다.
반복자 체인은 만들기, 변환하기, 소비하기가 분리되어 있으므로 체인의 마지막에 어떤 소비 지점이 있는지 확인하는 습관이 중요합니다.
예시
이제 모든 결과를 모으기 전에 한 번만 next를 호출해 봅시다. 다음 프로그램은 출력으로 실제 평가 순서를 드러내기 위한 작은 실험입니다. 일반적인 부수 효과 반복에는 for가 더 직접적이며, 여기서는 지연성을 관찰하려고 map 안에 출력을 넣습니다.
fn main() {
let mut values = vec![1, 2, 3].into_iter().map(|n| {
println!("mapping {n}");
n * 2
});
println!("built");
let first = values.next();
println!("first={first:?}");
let rest: Vec<_> = values.collect();
println!("rest={rest:?}");
}built
mapping 1
first=Some(2)
mapping 2
mapping 3
rest=[4, 6]체인 생성은 반복자와 캡처 환경을 만들지만, 이 map의 본문은 아직 호출하지 않습니다. 첫 next가 한 원소를 요청한 뒤에는 그 위치가 진행되어 있으므로 collect는 처음부터 다시 시작하지 않고 남은 두 원소를 모읍니다. 반복자를 소비하는 것과 원본 컬렉션을 소비하는 것도 구분해야 합니다. v1.iter().sum()은 반복자를 소유해 진행하지만 원본 v1은 공유로 빌립니다. 위의 소유 Vec에 대한 into_iter()는 원소를 내보낼 권한까지 옮깁니다.
필터의 두 참조 층은 반환값과 검사 인수를 나누면 이해하기 쉽습니다. iter()의 Item이 &i32라면 filter의 검사 인수는 &&i32입니다. 반면 신발 예제의 소유 into_iter()는 Item = Shoe, 검사 인수는 &Shoe입니다. 검사할 때만 빌린 후 통과한 원래 Shoe를 결과로 옮기므로 결과가 Vec<&Shoe>로 바뀌거나 신발을 복제하는 것은 아닙니다. Iterator::filter의 계약에서 &Self::Item과 반환 Item을 따로 읽어 보세요.
스스로 따라가기
정수 벡터 [1, 2, 3, 4]를 소유 into_iter()로 넘기고 map(|n| n * 2).filter(|n| *n > 4) 체인을 만듭니다. 체인 생성 시점, 첫 next, 이어 두 번째 next에서 반환하는 값과 그때까지 검사한 원본 원소를 적으세요. 그 뒤 collect::<Vec<_>>()의 결과는 무엇인가요? 같은 코드를 iter()로 시작했다면 원본 벡터를 쓸 수 있는지와 filter 인수의 타입도 구분하세요.
풀이 확인
생성만 했을 때는 아직 원소를 검사하지 않습니다. 첫 next는 원본 1·2를 각각 2·4로 바꾸어 제외한 뒤, 원본 3을 6으로 바꾸어 Some(6)을 반환합니다. 한 결과를 얻으려고 원본 세 원소를 진행한 것입니다. 두 번째 next는 원본 4를 진행해 Some(8)을 반환합니다. 이제 남은 원소가 없으므로 이어 수집하는 벡터는 비어 있습니다.
이 체인의 map이 새 i32를 반환하므로 into_iter()와 iter() 어느 쪽이든 그 뒤 filter 검사 인수는 &i32입니다. iter() 바로 뒤에 filter를 붙일 때의 &&i32와 혼동하지 마세요. iter()로 시작하면 공유 빌림이 끝난 뒤 원본 벡터를 다시 사용할 수 있습니다. 소유 벡터의 into_iter()로 시작하면 그 벡터 자체는 이동된 상태입니다.
반복자로 일련의 아이템들 처리하기 학습 판단 기준표
반복자로 일련의 아이템들을 처리하는 방법의 마지막 점검은 실제 호출 모양과 실패 처리 경계가 자연스럽게 이어지는지에 맞춥니다.