반복자로 항목 시리즈 처리하기
반복자 패턴은 어떤 항목들의 시퀀스에 대해 순서대로 작업을 수행하게 해 줍니다. 반복자는 각 항목을 어떻게 순회할지, 그리고 언제 시퀀스가 끝나는지를 결정하는 로직을 담당합니다. 반복자를 사용하면 그런 로직을 직접 다시 구현할 필요가 없습니다.
러스트에서 반복자는 게으르다(lazy) 는 특징이 있습니다. 즉, 반복자를 소모하는
메서드를 호출해 실제로 사용하기 전까지는 아무 일도 하지 않습니다. 예를 들어
목록 13-10의 코드는 Vec<T> 에 정의된 iter 메서드를 호출해 벡터 v1 의 항목에
대한 반복자를 만들지만, 이 코드만으로는 아무 유용한 일도 하지 않습니다.
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
}
반복자는 v1_iter 변수에 저장됩니다. 반복자를 만들고 나면 여러 방식으로 사용할 수
있습니다. 목록 3-5에서는 for 루프로 배열을 순회하며 각 항목에 어떤 코드를 실행하는
예를 보았습니다. 그때 내부적으로는 반복자가 암묵적으로 만들어지고 소비되었지만,
정확히 어떻게 동작하는지는 지금까지 자세히 보지 않았습니다.
목록 13-11의 예에서는 반복자 생성과 반복자 사용을 for 루프에서 분리해서 보여 줍니다.
for 루프가 v1_iter 안의 반복자를 사용하면, 반복자의 각 요소가 루프의 한 번의
반복에서 사용되어 각 값을 출력합니다.
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}
}
for 루프에서 반복자 사용하기표준 라이브러리가 반복자를 제공하지 않는 언어라면, 같은 기능을 구현하기 위해 인덱스 변수를 0에서 시작하고, 그 변수로 벡터에서 값을 꺼내며, 루프 안에서 인덱스를 하나씩 증가시키다가 벡터 항목 수에 도달하면 멈추는 코드를 직접 써야 했을 것입니다.
반복자는 그런 로직을 모두 대신 처리해 주므로, 반복해서 쓰다가 실수할 수도 있는 코드를 줄여 줍니다. 또한 벡터처럼 인덱스로 접근할 수 있는 자료구조뿐 아니라 다양한 종류의 시퀀스에 같은 로직을 유연하게 적용할 수 있게 해 줍니다. 이제 반복자가 어떻게 이를 가능하게 하는지 살펴보겠습니다.
Iterator 트레이트와 next 메서드
모든 반복자는 표준 라이브러리에 정의된 Iterator 라는 트레이트를 구현합니다.
이 트레이트 정의는 다음과 같습니다.
#![allow(unused)]
fn main() {
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}
}
이 정의에는 새로운 문법인 type Item 과 Self::Item 이 등장한다는 점에 주목하세요.
이것은 이 트레이트에 대한 연관 타입(associated type)을 정의하는 문법입니다. 연관 타입은
20장에서 자세히 다룹니다. 지금은 이 코드가, Iterator 트레이트를 구현하려면
Item 타입도 함께 정의해야 하며, 이 Item 타입이 next 메서드의 반환 타입에
사용된다고 말하고 있다는 점만 이해하면 충분합니다. 다시 말해 Item 타입은 반복자가
반환하는 값의 타입이 됩니다.
Iterator 트레이트는 구현자에게 단 하나의 메서드만 요구합니다. 바로 next
메서드입니다. next 는 반복자의 항목을 한 번에 하나씩 반환하고, 항목이 남아 있을 때는
Some 으로 감싸 돌려주며, 순회가 끝났을 때는 None 을 반환합니다.
반복자에 대해 next 메서드를 직접 호출할 수도 있습니다. 목록 13-12는 벡터에서 만든
반복자에 대해 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);
}
}
next 메서드 호출하기여기서 v1_iter 를 가변으로 선언해야 했다는 점에 주의하세요. 반복자에 대해 next
메서드를 호출하면 반복자가 현재 시퀀스에서 어느 위치에 있는지를 추적하는 내부 상태가
변하기 때문입니다. 즉, 이 코드는 반복자를 소모(consume) 합니다. next 를 호출할
때마다 반복자 안의 항목 하나가 사용됩니다. for 루프를 사용할 때는, 루프가 내부에서
v1_iter 소유권을 가져가고 가변으로 처리해 주었기 때문에 직접 mut 를 붙일
필요가 없었습니다.
또한 next 호출로 얻는 값이 벡터 안 값에 대한 불변 참조라는 점에도 주목하세요.
iter 메서드는 불변 참조에 대한 반복자를 만듭니다. 만약 v1 의 소유권을 가져가고
소유된 값을 반환하는 반복자를 만들고 싶다면 iter 대신 into_iter 를 호출할 수
있습니다. 마찬가지로, 가변 참조를 순회하고 싶다면 iter 대신 iter_mut 를 사용할
수 있습니다.
반복자를 소비하는 메서드
Iterator 트레이트에는 표준 라이브러리가 기본 구현까지 제공하는 다양한 메서드가
정의되어 있습니다. 이 메서드들이 어떤 것인지는 Iterator 트레이트에 대한 표준
라이브러리 API 문서를 보면 확인할 수 있습니다. 이 메서드들 중 일부는 내부 구현에서
next 를 호출하기 때문에, Iterator 트레이트를 구현할 때 next 메서드를 반드시
정의해야 하는 것입니다.
이처럼 next 를 호출하는 메서드는 반복자를 사용해 버리기 때문에 소비 어댑터(consuming
adapters) 라고 부릅니다. 한 예가 sum 메서드입니다. sum 은 반복자의 소유권을
가져와, next 를 반복해서 호출하며 항목들을 순회하고, 순회하면서 각 항목을 누적값에
더한 뒤 모든 순회가 끝나면 그 총합을 반환합니다. 목록 13-13은 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 메서드를 호출해 반복자 안 모든 항목의 합 구하기sum 이 호출된 뒤에는 v1_iter 를 더 이상 사용할 수 없습니다. sum 이 우리가
호출한 반복자의 소유권을 가져가기 때문입니다.
다른 반복자를 만들어 내는 메서드
반복자 어댑터(iterator adapters) 는 Iterator 트레이트에 정의된 메서드로,
반복자를 소비하지는 않습니다. 대신 원래 반복자의 어떤 성질을 바꾼 새로운 반복자를
만들어 냅니다.
목록 13-14는 map 이라는 반복자 어댑터 메서드를 호출하는 예입니다. map 은 각
항목을 순회할 때마다 적용할 클로저를 하나 받습니다. 그리고 수정된 항목들을 생성하는
새 반복자를 반환합니다. 여기의 클로저는 원래 벡터 각 항목에 1을 더한 값들을 만들어
내는 새 반복자를 생성합니다.
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
}
map 호출하기하지만 이 코드는 경고를 냅니다.
$ 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
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++
warning: `iterators` (bin "iterators") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
목록 13-14의 코드는 실제로 아무 일도 하지 않습니다. 우리가 지정한 클로저는 한 번도 호출되지 않습니다. 왜냐하면 반복자 어댑터는 게으르기 때문입니다. 이 경고는 그 사실을 상기시켜 줍니다. 즉, 여기서는 반복자를 실제로 소비해야만 의미 있는 일이 일어납니다.
이 경고를 없애고 반복자를 소비하기 위해, 목록 12-1에서 env::args 와 함께 사용했던
collect 메서드를 다시 사용하겠습니다. 이 메서드는 반복자를 소비하고, 그 결과값들을
컬렉션 타입 하나로 모읍니다.
목록 13-15에서는 map 호출이 반환한 반복자를 순회한 결과를 벡터로 모읍니다. 이
벡터는 원래 벡터의 각 항목에 1이 더해진 값을 담게 됩니다.
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 으로 새 반복자를 만들고, collect 로 그 반복자를 소비해 벡터 생성하기map 이 클로저를 받기 때문에, 각 항목에 대해 수행하고 싶은 동작을 마음대로 지정할
수 있습니다. 이는 Iterator 트레이트가 제공하는 반복 동작을 재사용하면서도, 클로저를
통해 일부 동작을 원하는 대로 맞춤화할 수 있다는 아주 좋은 예입니다.
여러 반복자 어댑터 호출을 체이닝해 복잡한 작업을 읽기 좋게 표현할 수도 있습니다. 다만 모든 반복자가 게으르기 때문에, 반복자 어댑터 호출 결과에서 실제 값을 얻으려면 반드시 소비 어댑터 메서드 하나를 마지막에 호출해야 합니다.
환경을 캡처하는 클로저
많은 반복자 어댑터는 인수로 클로저를 받고, 우리가 반복자 어댑터에 넘기는 클로저는 대개 환경을 캡처하는 클로저가 됩니다.
예제로는 filter 메서드를 사용하겠습니다. filter 는 클로저를 받는데, 그 클로저는
반복자에서 항목 하나를 받아 bool 값을 반환합니다. 클로저가 true 를 반환하면
그 값은 filter 가 만들어 내는 반복자 안에 포함됩니다. false 를 반환하면 그 값은
제외됩니다.
목록 13-16에서는 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")
},
]
);
}
}
shoe_size 를 캡처하는 클로저와 함께 filter 메서드 사용하기shoes_in_size 함수는 신발 벡터의 소유권과 원하는 신발 사이즈를 매개변수로 받습니다.
그리고 지정한 사이즈의 신발만 담은 벡터를 반환합니다.
함수 본문에서는 먼저 into_iter 를 호출해, 신발 벡터의 소유권을 가져가는 반복자를
만듭니다. 그 다음 filter 를 호출해, 클로저가 true 를 반환하는 요소만 포함하는
새 반복자로 바꿉니다.
이 클로저는 환경에서 shoe_size 매개변수를 캡처하고, 각 신발의 size 필드와
비교합니다. 그래서 지정한 사이즈와 같은 신발만 남게 됩니다. 마지막으로 collect 를
호출해 어댑터 반복자가 반환하는 값들을 벡터로 모으고, 그 벡터를 함수에서 반환합니다.
이 테스트는 shoes_in_size 를 호출했을 때, 우리가 지정한 크기와 같은 신발만 돌아오는지
확인합니다.