콘텐츠로 이동

Rust 반복자

반복자(iterator)는 값을 하나씩 차례대로 만들어 내는 것을 말해요. 사실 for 반복문 페이지부터 이미 쓰고 있었어요. 이름을 몰랐을 뿐이에요. 컬렉션을 도는 for 반복문은 모두 뒤에서 반복자가 움직이고 있거든요.

fn main() {
let numbers = vec![1, 2, 3];
let mut iter = numbers.iter();
println!("{:?}", iter.next()); // Some(1)
println!("{:?}", iter.next()); // Some(2)
println!("{:?}", iter.next()); // Some(3)
println!("{:?}", iter.next()); // None
}

실행 결과:

Some(1)
Some(2)
Some(3)
None
▶ 직접 실행해 보기

.next()는 Option을 돌려줘요. 아이템이 있으면 Some(값), 다 떨어지면 None이에요. for 반복문도 속을 들여다보면 None이 나올 때까지 .next()를 계속 호출하는 것뿐이에요.

.map()은 클로저를 받아서 모든 아이템에 적용하고, 새 반복자를 만들어요.

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
}

실행 결과:

[2, 4, 6, 8, 10]
▶ 직접 실행해 보기

.collect()는 반복자를 다시 실제 컬렉션(여기서는 Vec<i32>)으로 바꿔 줘요. 이게 없으면 아직 아무것도 계산하지 않은 반복자만 덩그러니 남아 있게 돼요.

fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = numbers.iter().filter(|n| **n % 2 == 0).collect();
println!("{:?}", evens);
}

실행 결과:

[2, 4, 6]
▶ 직접 실행해 보기

**n처럼 별표가 두 개라 낯설어 보일 거예요. .filter()는 클로저에 참조자의 참조자(&&i32)를 넘겨주기 때문에, 일반 숫자와 비교하려면 역참조를 두 번 해야 해요. 자주 마주치는 작은 특징이니, 지금은 자세한 이유까지 몰라도 괜찮아요.

반복자 메서드를 이어 붙이면(체이닝) 진가가 드러나요. 여러 단계를 읽기 쉬운 파이프라인 하나로 묶을 수 있거든요.

fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers
.iter()
.filter(|n| **n % 2 == 0) // 짝수만 남기기
.map(|n| n * 10) // 각각 10 곱하기
.collect();
println!("{:?}", result);
}

실행 결과:

[20, 40, 60]
▶ 직접 실행해 보기

같은 로직을 for 반복문과 빈 Vec에 push하는 방식으로 써 봐도 돼요. 틀린 방법은 아니에요. 하지만 체이닝한 쪽이 “거르고, 곱하고, 모은다”라는, 머릿속으로 생각한 순서 그대로 읽혀요.

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let total: i32 = numbers.iter().sum();
let count = numbers.iter().count();
let has_even = numbers.iter().any(|n| n % 2 == 0);
println!("합계: {total}, 개수: {count}, 짝수 있음: {has_even}");
}

실행 결과:

합계: 15, 개수: 5, 짝수 있음: true
▶ 직접 실행해 보기

.sum()은 모두 더하고, .count()는 아이템이 몇 개인지 알려 주고, .any()는 “조건에 맞는 아이템이 하나라도 있나요?“에 답해요. 이건 모든 반복자가 가진 메서드 중 일부일 뿐이에요. enumerate, zip, take, rev처럼 훨씬 많아요. 사용자 정의 반복자 페이지에서는 반복자를 직접 만들어 보고, 그 반복자도 이 메서드들을 공짜로 얻는다는 걸 확인할 거예요.

1부터 10까지의 숫자 중 홀수만 남기고, 각각 제곱한 다음, 모두 더해 보세요. mut 변수 없이 반복자 체인 하나로 해 보세요. 힌트: .filter(), .map(), .sum()을 이어 붙이고, 결과에는 let total: i32처럼 타입을 적어 주세요.

정답 보기
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let total: i32 = numbers
.iter()
.filter(|n| **n % 2 == 1) // 1, 3, 5, 7, 9만 남기기
.map(|n| n * n) // 1, 9, 25, 49, 81
.sum();
println!("홀수 제곱의 합: {total}");
}

실행 결과:

홀수 제곱의 합: 165
▶ 직접 실행해 보기

total에 : i32가 필요한 이유는 .sum()이 여러 숫자 타입으로 더할 수 있어서, 어떤 타입을 원하는지 Rust가 추측할 수 없기 때문이에요.

vec!["the", "quick", "brown", "fox", "jumps"]에서 시작해요. 3글자보다 긴 단어만 대문자로 바꿔 새 Vec<String>을 만들고, {:?}로 출력해 보세요. 힌트: .filter(|w| w.len() > 3)로 긴 단어만 남기고, .to_uppercase()로 &str을 대문자 String으로 바꿀 수 있어요.

정답 보기
fn main() {
let words = vec!["the", "quick", "brown", "fox", "jumps"];
let loud: Vec<String> = words
.iter()
.filter(|w| w.len() > 3)
.map(|w| w.to_uppercase())
.collect();
println!("{:?}", loud);
}

실행 결과:

["QUICK", "BROWN", "JUMPS"]
▶ 직접 실행해 보기

words 자체는 바뀌지 않아요. .iter()는 words를 빌리기만(대여) 하니까, 체인이 끝난 뒤에도 words를 계속 쓸 수 있어요.

이 문제의 단어는 영어 그대로 두었어요. .len()은 글자 수가 아니라 바이트 수를 세는데, 한글은 한 글자가 3바이트라서 결과가 달라지거든요.

  • 반복자는 .next()로 값을 하나씩 만들어 내고, .next()는 Option을 돌려줘요.
  • for 반복문도 속에서는 반복자로 동작해요.
  • .map()은 각 아이템을 바꾸고, .filter()는 조건에 맞는 것만 남기고, .collect()는 결과를 다시 실제 컬렉션으로 만들어요.
  • 반복자 메서드를 이어 붙이는 쪽이, 가변 누적 변수를 쓰는 수동 반복문보다 대개 더 알아보기 쉬워요.

이해도 점검

1. 반복자에서 .next()는 무엇을 돌려줄까요?

2. .iter().map(...).collect() 같은 체인 끝의 .collect()는 무슨 일을 할까요?

3. .filter(|n| **n % 2 == 0)에서 n을 왜 두 번 역참조할까요?

4. for 반복문이 컬렉션을 돌 때, 뒤에서 무엇이 움직일까요?

5. .filter()를 거친 반복자에는 무엇이 남을까요?

점수: 0 / 5