반복자로 항목 시퀀스 처리하기
반복자로 항목 시퀀스 처리하기 (Iterators)
반복자 패턴은 항목 시퀀스에 대해 어떤 작업을 차례대로 수행할 수 있게 해줘요. 반복자(iterator)는 각 항목을 하나씩 살펴보고 시퀀스가 끝났는지를 판단하는 로직을 책임져요. 반복자를 쓰면 그 로직을 직접 재구현할 필요가 없어요.
러스트에서 반복자는 lazy해요. 즉 반복자를 소비하는 메서드를 호출해 실제로 사용하기 전까지는 아무 효과가 없다는 뜻이에요. 예를 들어 Listing 13-10의 코드는 Vec<T>에 정의된 iter 메서드를 호출해 벡터 v1의 항목들에 대한 반복자를 만들어요. 이 코드만으로는 아무 유용한 일도 하지 않아요.
Filename: src/main.rs
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
}
Listing 13-10: 반복자 만들기
반복자는 v1_iter 변수에 저장돼요. 반복자를 만들면 다양한 방식으로 쓸 수 있어요. Listing 3-5에서 배열을 for 루프로 순회하며 각 항목에 어떤 코드를 실행했었죠. 겉으로 보기에는 그게 implicitly 반복자를 만들고 소비한 것이었는데, 여기까지 정확히 어떻게 동작하는지는 넘겨짚고 왔어요.
Listing 13-11의 예시에서는 반복자 생성과 for 루프에서의 사용을 분리했어요. v1_iter의 반복자를 for 루프로 사용하면, 반복자의 각 요소가 루프의 한 번의 반복에 쓰여 각 값을 출력해요.
Filename: src/main.rs
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}
}
Listing 13-11: for 루프에서 반복자 사용하기
표준 라이브러리가 반복자를 제공하지 않는 언어에서는 이와 같은 기능을, 변수를 인덱스 0에서 시작해 그 변수로 벡터를 인덱싱해 값을 얻고, 변수가 벡터 항목 수에 도달할 때까지 루프에서 변수 값을 증가시키는 방식으로 직접 작성해야 할 거예요.
반복자는 그 모든 로직을 알아서 처리하므로, 실수하기 쉬운 반복 코드를 줄여줘요. 또 같은 로직을 벡터처럼 인덱싱할 수 있는 데이터 구조만이 아니라 다양한 종류의 시퀀스에 쓸 수 있는 유연성도 줘요. 반복자가 어떻게 그렇게 하는지 살펴볼게요.
출처: The Rust Book
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 트레이트는 구현자에게 한 번에 반복자의 항목 하나를 Some에 담아 반환하고, 순회가 끝나면 None을 반환하는 next 메서드 단 하나만 정의하도록 요구해요.
반복자에 next 메서드를 직접 호출할 수 있어요. Listing 13-12는 벡터로 만든 반복자에 next를 반복 호출했을 때 어떤 값이 반환되는지 보여줘요.
Filename: src/lib.rs
#[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);
}
}
Listing 13-12: 반복자에 next 메서드 호출하기
v1_iter를 가변으로 만들어야 했다는 점을 눈여겨보세요. 반복자에 next 메서드를 호출하는 것은 반복자가 시퀀스의 어디쯤 있는지 추적하는 내부 상태를 바꾸기 때문이에요. 즉 이 코드는 반복자를 소비(consume), 다시 말해 사용해 버려요. next를 호출할 때마다 반복자에서 항목 하나를 먹어 치우죠. for 루프를 쓸 때는 v1_iter를 가변으로 만들 필요가 없었어요. 루프가 v1_iter의 소유권을 가져가서 뒤에서 가변으로 만들기 때문이에요.
next 호출로 얻는 값이 벡터 값에 대한 불변 참조라는 점도 주목하세요. iter 메서드는 불변 참조에 대한 반복자를 만들어요. v1의 소유권을 가져가 소유한 값을 반환하는 반복자를 만들려면 iter 대신 into_iter를 호출하면 돼요. 마찬가지로 가변 참조로 순회하려면 iter 대신 iter_mut을 호출하면 되고요.
반복자를 소비하는 메서드 (Methods That Consume the Iterator)
Iterator 트레이트에는 표준 라이브러리가 기본 구현을 제공하는 여러 메서드가 있어요. 이런 메서드들의 목록은 표준 라이브러리 API 문서의 Iterator 트레이트에서 찾아볼 수 있어요. 이 중 일부는 정의에서 next 메서드를 호출하는데, 그래서 Iterator 트레이트를 구현할 때 next 메서드를 구현해야 하는 거예요.
next를 호출하는 메서드를 **소비 어댑터(consuming adapter)**라고 불러요. 호출하면 반복자를 사용해 버리기 때문이죠. 한 예로 sum 메서드가 있어요. 이 메서드는 반복자의 소유권을 가져가 next를 반복 호출하며 항목들을 순회해 반복자를 소비해요. 순회하면서 각 항목을 누적 합계에 더하고, 순회가 끝나면 그 합계를 반환해요. Listing 13-13에 sum 메서드 사용을 보여주는 테스트가 있어요.
Filename: src/lib.rs
#[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);
}
}
Listing 13-13: sum 메서드를 호출해 반복자의 모든 항목 합계 구하기
sum은 호출한 반복자의 소유권을 가져가므로, sum 호출 이후에는 v1_iter를 사용할 수 없어요.
다른 반복자를 만들어내는 메서드 (Methods That Produce Other Iterators)
**반복자 어댑터(iterator adapter)**는 Iterator 트레이트에 정의된 메서드로, 반복자를 소비하지 않아요. 대신 원래 반복자의 어떤 측면을 바꿔 다른 반복자를 만들어내요.
Listing 13-14는 어댑터 메서드 map 호출 예시인데, map은 항목이 순회될 때 각 항목에 호출할 클로저를 받아요. map 메서드는 수정된 항목들을 만들어내는 새 반복자를 반환해요. 여기서 클로저는 벡터의 각 항목이 1씩 증가하는 새 반복자를 만들어요.
Filename: src/main.rs
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
}
Listing 13-14: 새 반복자를 만들기 위해 반복자 어댑터 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`
Listing 13-14의 코드는 아무것도 하지 않아요. 우리가 지정한 클로저는 절대 호출되지 않죠. 경고가 그 이유를 알려줘요. 반복자 어댑터는 lazy하고, 여기서 반복자를 소비해야 한다고요.
이 경고를 고치고 반복자를 소비하기 위해 collect 메서드를 쓸게요. collect는 Listing 12-1에서 env::args와 함께 썼었죠. 이 메서드는 반복자를 소비해 그 결과 값들을 컬렉션 데이터 타입에 모아요.
Listing 13-15에서는 map 호출에서 반환된 반복자를 순회한 결과를 벡터에 모아요. 이 벡터에는 원래 벡터의 각 항목이 1씩 증가된 값이 담기게 돼요.
Filename: src/main.rs
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]);
}
Listing 13-15: 새 반복자를 만들기 위해 map 메서드를 호출하고, collect 메서드를 호출해 새 반복자를 소비해 벡터 만들기
map은 클로저를 받으므로 각 항목에 대해 수행하고 싶은 어떤 작업이든 지정할 수 있어요. 이것은 클로저가 Iterator 트레이트가 제공하는 순회 동작을 재사용하면서 어떤 동작을 커스터마이즈하게 해준다는 좋은 예시예요.
반복자 어댑터를 여러 번 체이닝하면 복잡한 동작을 읽기 쉽게 수행할 수 있어요. 하지만 모든 반복자가 lazy하기 때문에, 반복자 어댑터 호출의 결과를 얻으려면 소비 어댑터 메서드 중 하나를 호출해야 해요.
환경을 캡처하는 클로저 (Closures That Capture Their Environment)
많은 반복자 어댑터가 클로저를 인자로 받는데, 반복자 어댑터에 인자로 지정하는 클로저는 흔히 환경을 캡처하는 클로저예요.
이 예시에서는 클로저를 받는 filter 메서드를 쓸게요. 이 클로저는 반복자에서 항목 하나를 받아 bool을 반환해요. 클로저가 true를 반환하면 그 값이 filter가 만들어내는 순회에 포함되고, false를 반환하면 포함되지 않아요.
Listing 13-16에서는 환경에서 shoe_size 변수를 캡처하는 클로저와 함께 filter를 사용해 Shoe 구조체 인스턴스 컬렉션을 순회해요. 지정한 사이즈의 신발만 반환하게 되죠.
Filename: src/lib.rs
#[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")
},
]
);
}
}
Listing 13-16: shoe_size를 캡처하는 클로저와 함께 filter 메서드 사용하기
shoes_in_size 함수는 신발 벡터와 신발 사이즈를 매개변수로 받아 소유권을 가져가요. 지정된 사이즈의 신발만 담긴 벡터를 반환하죠.
shoes_in_size 본문에서 into_iter를 호출해 벡터의 소유권을 가져가는 반복자를 만들어요. 그런 다음 filter를 호출해 그 반복자를, 클로저가 true를 반환하는 요소만 담긴 새 반복자로 어댑트해요.
클로저는 환경에서 shoe_size 매개변수를 캡처해 각 신발 사이즈와 그 값을 비교하고, 지정된 사이즈의 신발만 남겨요. 마지막으로 collect를 호출해 어댑트된 반복자가 반환한 값들을 함수가 반환하는 벡터로 모아요.
테스트는 shoes_in_size를 호출하면 우리가 지정한 값과 같은 사이즈의 신발만 돌아오는 걸 보여줘요.