Penflip

Penflip - Write better with others

  • Loading...
  • Discover Projects
  • Help
  • Signup
  • Login
  • Welcome back!
    No account? Signup Close
    Ready to write better?
    Have an account? Login Close

sarojaba · Rust Doc Korean

Make Changes
46

4.5. 반복자 (Iterators) - 100%

반복문에 대해 이야기 해 봅시다.

Rust 의 for 반복문을 기억하시나요? 여기 예제가 있습니다:

for x in 0..10 {
    println!("{}", x);
}

지금부터 Rust 에 대해 좀더 알게 될테데요, 어떻게 동작하는 지에 대해서는 나중에 상세히 다룹니다.
0..10 로 표기된 범위(Range)를 '반복자(iterator)' 라고 합니다. 반복자란 계속해서 .next() 메소드를 호출할 수 있으며, 연속된 것들을 돌려주는 어떤 것을 말합니다.

이런거죠:

let mut range = 0..10;

loop {
    match range.next() {
        Some(x) => {
            println!("{}", x);
        },
        None => { break }
    }
}

범위로 만든 반복자에 대해 변경 가능한(mutable) 바인딩을 만들었습니다.
loop 안에서 range.next() 에 대해 match 를 사용하여 반복자의 다음 값에 대한 레퍼런스를 얻습니다.
next 는 Option<i32> 타입을 리턴하는데, 이 경우, 값이 있다면 Some(i32) 이 되며, 모든 값을 순회했다면 None 이 될 것 입니다. 리턴값이 Some(i32) 이면 출력하고, None 이면 break 를 사용해서 반복문을 빠져나갑니다.

이 예제는 기본적으로 for 반복문 버전과 동일합니다. for 반복문은 loop/match/break 를 사용하는 것 보다 쉬운 방법 입니다.

for 순환에서만 반복자를 사용하는 것은 아닙니다. 이 가이드의 범위를 벗어나긴 합니다만, Iterator 특성(trait)을 구현하여 자신만의 반복자를 만들 수 있습니다. Rust 는 다양한 작업에 쓸 수 있는 다수의 유용한 반복자들을 제공합니다. 반복자들에 대해 이야기 하기 전에, Rust 에서 피해야할 패턴(anti-pattern)들에 대해 이야기 하는 것이 좋을 것 같습니다. 아래와 같이 범위(range)를 사용하는 것이 그 중 하나 입니다.

범위는 멋지지만 동시에 매우 원시적입니다. 예를 들면, 벡터의 내용을 순회하려고 할 때 아래와 같이 쓸 수 있습니다:

let nums = vec![1, 2, 3];

for i in 0..nums.len() {
    println!("{}", nums[i]);
}

이 방식은 실제로 반복자를 사용하는 것보다 좋지 못합니다. 벡터를 아래 처럼 직접 순회할 수 있습니다:

let nums = vec![1, 2, 3];

for num in &nums {
    println!("{}", num);
}

이렇게 사용하는 두 가지 이유가 있는데요.
첫째, 의도하는 바를 더 직접적으로 표현 합니다. 벡터를 색인(index)으로 접근하면서 순회하기 보다, 전체 벡터를 순회합니다.
둘째, 이렇게 사용하는 것이 더 효과적입니다: nums[i] 처럼 색인을 사용하기 때문에 범위 체크가 필요합니다. 반복자를 사용해서 벡터의 각 요소에 대한 참조를 가져오기 때문에, 범위 체크가 필요 없습니다. 보퉁 반복자는 이렇게 사용합니다: 불필요한 범위 체크를 무시하면서도, 안전하게 됩니다.

여기 다른 상세한 내용이 있는데, println! 의 동작 때문에 100% 명확하진 않습니다. num 은 &i32 타입 입니다. 이 말은 i32 에 대한 참조라는 것이지 i32 자체라는 것이 아닙니다. println! 은 알아서 역참조를 처리하지만 보이진 않습니다. 이 코드 역시 잘 동작 합니다:

let nums = vec![1, 2, 3];

for num in &nums {
    println!("{}", *num);
}

여기서는 명시적으로 num 을 역참조 합니다. &nums 는 왜 참조를 줄까요?
첫째로, 명시적으로 & 를 사용해서 요청 했습니다.
둘째로, 데이터를 받는다면 이것에 대한 소유자가 되어야 하며, 복사본을 받는 것 입니다.

참조를 사용하면, 데이터에 대한 참조를 빌려올 뿐 이동이 필요 없습니다.

이제 범위가 보통 필요로 하는 것이 아니라은 것을 알았습니다. 대신 어떤 것들이 필요한지 이야기 해 봅시다.

여기 적당한 세 개의 광대한 클래스가 있습니다: 반복자(iterator), 반복자 어댑터, 소비. 여기 정의가 있습니다.

  • 반복자(iterator) 는 연속적인 값을 줍니다.
  • 반복자 어댑터(iterator adapter) 는 반복자에 대해 동작하며, 다른 연속적인 출력을 위한 새로운 반복자를 만듭니다.
  • 소비자(consumer) 는 반복자에 대해 동작하며, 최종 값의 집합을 만듭니다.

반복자와 범위를 이미 봤으니, 소비자에 대해 먼저 이야기 해 봅시다.

소비자(Consumers)

소비자 는 반복자에 대해 동작하며, 어떤 값 혹은 값들을 돌려줍니다.
가장 흔한 소비자는 collect() 입니다. 이 코드는 컴파일 되지 않지만, 어떤 의도가 있는지 알 수 있습니다:

let one_to_one_hundred = (1..101).collect();

보이는 것 처럼 반복자에 대해 collect() 를 호출합니다.
collect() 는 반복자가 줄 수 있는한 많은 값을 가져가서, 값 뭉치를 돌려줍니다.
그런데 왜 컴파일 되지 않을 까요? Rust 는 어떤 타입의 값을 모아야 하는지 알 수 없기 때문에, 알려줘야 합니다.
여기 컴파일 되는 버전이 있습니다:

let one_to_one_hundred = (1..101).collect::<Vec<i32>>();

::<> 문법을 사용해서 타입힌트를 줌으로써, 정수들을 갖는 vector 를 원한다는 것을 알려줄 수 있습니다.
항상 전체 타입을 기술할 필요는 없습니다. _ 를 사용해서 부분적인 힌트를 줄 수 도 있습니다:

let one_to_one_hundred = (1..101).collect::<Vec<_>>();

이 것은 "Vec<T> 에 모아주세요, 그런데 T 가 어떤 타입인지는 추론 해주세요." 라고 말하는 것입니다.
이런 이유에서 _ 는 type placeholder 라고도 합니다.

collect() 가 가장 흔한데, 여기 다른 것도 있습니다. find() 입니다:

let greater_than_forty_two = (0..100)
                             .find(|x| *x > 42);

match greater_than_forty_two {
    Some(_) => println!("We got some numbers!"),
    None => println!("No numbers found :("),
}

find 는 클로저(closure)를 인자로 받고, 반복자의 각 요소에 대한 참조를 사용합니다.
만약 찾고자 하는 요소가 있다면, 클로져는 true 를 리턴하고, 그 외에는 false 를 리턴 합니다. 일치하는 요소를 찾는 것이 아니기 때문에, find는 요소 자체 보다는 Option 을 돌려줍니다.

또 다른 중요한 소비자는 fold 입니다. 이렇게 생겼습니다:

let sum = (1..4).fold(0, |sum, x| sum + x);

fold() 는 이런 형태 입니다:
fold(base, |accumulator, element| ...). 두 개의 인자를 받습니다.
첫 번째 인자는 base 라고 합니다.
두 번째는 두 개의 인자를 받는 클로져 입니다: 첫 번째는 누적기(accumulator) 라고 합니다. 두 번째는 요소(element) 입니다.
각 순회 마다 클로져가 호출되고, 클로져의 결과값은 다음 순회에 누적기 인자로 넘어갑니다. 최초 순회에서 base 는 누적기의 초기 값으로 사용됩니다.

약간 혼동 되겠지만, 이 반복자에서 모든 값들이 어떻게 변하는지 검사해 봅시다:

base accumulator element closure result
0 0 1 1
0 1 2 3
0 3 3 6

아래와 같이 fold() 를 호출 했습니다:

# (1..4)
.fold(0, |sum, x| sum + x);

0 는 base 이고, sum 은 누적기, x 는 요소 입니다.
첫 번째 순회에서 sum 는 0, x 는 nums 의 첫 번째 요소 1.
그 다음 sum 과 x 를 더하면, 0 + 1 = 1 이 됩니다.
두 번째 순회에서 이 값은 누적기인 sum 의 값이 되고, x 는 배열이 두 번째 요소 2 가 됩니다.
1 + 2 = 3 은 마지막 순회에서 누적기의 값이 됩니다.
마지막 순회에서 x 는 마지막 요소인 3 이 되고, 3 + 3 = 6 이 최종 결과가 됩니다.

fold 는 처음에 좀 이상해 보일 수 있지만, 한번 적응이 되면 곳곳에 쓰게 됩니다.
일련의 처리 대상들이 있고 하나의 결과값이 필요할 때, fold 가 적절합니다.

소비자는 아직 다루지 않은 반복자의 추가적인 특성 때문에 중요합니다: 게으름(laziness)
반복자에 대해 더 알아보고, 왜 중요한지 알아 봅시다.

반복자 (Iterators)

이전에 이야기 했던 것 처럼, 반복자는 반복적으로 .next()를 호출할 수 있고 일련의 요소들을 돌려주는 어떤 것을 말합니다.
다음 값을 얻으려면 메소드를 호출 해줘야 하기 때문에, 반복자는 미리 모든 값을 만들어 놓을 필요 없이 게을러(lazy) 질 수 있습니다. 예를 들면, 아래 예제는 1-99 사이의 수를 실제로 생성하는 대신 연속적인 어떤 것이라는 값을 만듭니다.

let nums = 1..100;

범위(range)로 아무것도 하지 않기 때문에, 값을 생성하지 않습니다. 소비자를 붙여 봅시다;

let nums = (1..100).collect::<Vec<i32>>();

이제, collect() 호출은 값들을 넘겨 받아야 하기 때문에, 범위(range)는 숫자들을 생성할 것입니다.

범위(range)는 앞으로 보게될 기본적인 두 개의 반복자 중에 하나 입니다. 다른 하나는 iter() 입니다.
iter()는 벡터를 단순한 반복자로 변환하는데, 이 반복자는 한번에 하나씩 요소를 돌려 줍니다.

let nums = vec![1, 2, 3];

for num in nums.iter() {
   println!("{}", num);
}

두 개의 기초적인 반복자는 필요한 작업을 잘 수행해 줍니다. 무한(infinite) 반복자 같은 고급 반복자도 있습니다.

이정도면 반복자에 대해서는 충분합니다. 반복자 어댑터는 반복자에 대해 이야히 할 때 필요한 마지막 개념 입니다.
살펴봅시다!

반복자 어댑터 (Iterator adapters)

반복자 어댑터는 반복자를 받아서 어떤 방식으로 수정한 다음, 새로운 반복자를 돌려줍니다.
가장 단순한 형태를 map 이라고 합니다;

(1..100).map(|x| x + 1);

map은 반복자에 대해 호출하는데, 각 요소의 레퍼런스를 인자로 받는 클로저(closure)를 호출하게 됨으로써 새로운 반복자를 생성합니다. 결국 2-100 사이의 숫자들을 돌려 줄 것 입니다. 거의 된 것 같은데, 컴파일해 보면 경고를 출력 할 것입니다.

warning: unused result which must be used: iterator adaptors are lazy and
         do nothing unless consumed, #[warn(unused_must_use)] on by default
(1..100).map(|x| x + 1);
 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

여기서 또 게으름(laziness)을 보게 되는데, 클로저는 실행되지 않습니다.
이 예제는 어떤 숫자도 출력하지 않습니다:

(1..100).map(|x| println!("{}", x));

반복자에 대해 클로저를 실행하려고 하다면, for 를 사용하는 편이 났습니다.

흥미로운 반복자가 많은데, taken(n) 은 원본 반복자의 n 개의 요소를 갖는 반복자를 돌려 줄 것 입니다.
여기서 원본 반복자는 변경되지 않는다는 것에 유의하세요. 이제 무한 반복자를 사용해 봅시다:

for i (1..).take(5) {
    println!("{}", i);
}

이렇게 출력될 것 입니다.

This will print

1
2
3
4
5

filter() 는 클로저를 인자로 받는 어댑터 입니다. 이 클로저는 true 혹은 false 를 돌려줍니다. filter() 가 돌려주는 새로운 반복자는 클로저가 true 로 리턴한 요소들을 갖고 있습니다:

for i in (1..100).filter(|&x| x % 2 == 0) {
    println!("{}", i);
}

이것은 1 ~ 100 사이의 짝수들을 출력 합니다.
(filter 는 순회하는 요소들을 소비하지 않기 때문에, 각 요소에 대한 참조를 넘겨줍니다. 그러므로 filter 서술부에서는 정수 값을 가져오기 위해 &x 를 사용합니다.)

세 가지를 연결해서 사용할 수 도 있습니다: 반복자를 만들고 어댑터를 몇 번 적용한 후, 결과를 소비합니다.
살펴봅시다.

6, 12, 18, 24, 30 을 포함한 벡터를 돌려줍니다.

(1..)
    .filter(|&x| x % 2 == 0)
    .filter(|&x| x % 3 == 0)
    .take(5)
    .collect::<Vec<i32>>();

유용하게 사용할 수 있는 반복자와 반복자 어댑터, 소비자에 대해 약간 맛을 봤습니다. 다른 유용한 반복자가 많으며 새로 작성할 수 도 있습니다. 반복자는 모든 종류의 리스트를 안전하고 효율적으로 다루기 위한 방법을 제공합니다.
처음엔 어색할 수도 있는데, 계속 사용하다 보면 중독될 것 입니다. 다른 반복자와 소비자들에 대한 전체 목록 입니다. 확인해 보세요. iterator module documentation.

Updated by sarojaba about 4 years (view history)
4.4. 문서화 (Documentation) - 20% 4.6. 동시성 (Concurrency) - 100%

Contents

    Rust 문서 한글화 프로젝트 1. 소개(Introduction) 2. 시작하기(Getting Started) - 20% 3. Rust 배우기 (Learn Rust) - 100% 3.1. 추리 게임 (Guessing Game) - 100% 3.2. 식사하는 철학자들 (Dining Philosophers) - 100% 3.3. 다른 언어에 Rust 포함하기 (Rust Inside Other Languages) - 100% 4. 효과적인 Rust (Effective Rust) - 100% 4.1. 스택과 힙 (The Stack and the Heap) - 100% 4.2. 테스팅 (Testing) - 100% 4.3. 조건부 컴파일 (Conditional Compilation) - 70% 4.4. 문서화 (Documentation) - 20% 4.5. 반복자 (Iterators) - 100% 4.6. 동시성 (Concurrency) - 90% 4.7. 오류 처리 (Error Handling) - 4% 4.8. Choosing your Guarantees - 0% 4.9. 외부 함수 인터페이스 (Foreign Function Interface) - 50% 4.9. Borrow 와 AsRef 4.11. 배포 채널 (Release Channels) - 100% 5. 문법과 의미 (Syntax and Semantics) - 100% 5.1. 변수 바인딩 (Variable Bindings) - 100% 5.2. 함수 (Functions) - 100% 5.3. 기본형 (Primitive Types) - 100% 5.4. 주석 (Comments) - 100% 5.5. 조건식 (if) - 100% 5.6. 반복 (Loops) - 100% 5.7. 소유권 (Ownership) - 100% 5.8. 참조와 빌림 (References and Borrowing) - 100% 5.9. 수명 (Lifetimes) - 100% 5.10. 가변성 (Mutability) - 100% 5.11. 구조체 (Structs) - 100% 5.12. 열거형 (Enums) - 100% 5.13. 정합 (Match) - 100% 5.14. 패턴 (Patterns) - 80% 5.15. 메소드 문법 (Method Syntax) - 100% 5.16. 벡터 (Vectors) - 100% 5.17. 문자열(Strings) - 100% 5.18. 제너릭 (Generics) - 100% 5.19. 트레잇 (Traits) - 100% 5.20. 드랍 (Drop) - 100% 5.21. if let - 100% 5.22. 트레잇 객체 (Trait Objects) - 75% 5.23. 클로저 (Closures) - 10% 5.24. 전역 함수 사용법 (Universal Function Call Syntax) - 0% 5.25. 크레이트들과(Crate) 모듈들(Module) - 0% 9. 용어 색인 (Concordance) Show all
    Discussions 5 Pending changes 5 Contributors
    Download Share

    Download

    Working...

    Downloading...

    Downloaded!

    Download more

    Error!

    Your download couldn't be processed. Check for abnormalities and incorrect syntax. We've been notified of the issue.

    Back

    Download PDF Download ePub Download HTML Download Word doc Download text Download source (archive)

    Close
1,373 Words
7,179 Characters

Share

Collaborators make changes on their own versions, and all changes will be approved before merged into the main version.

Close

Penflip is made by Loren Burton
in Los Angeles, California

Tweet

    About

  • Team
  • Pricing
  • Our Story

    Quick Start

  • Markdown
  • Penflip Basics
  • Working Offline

    Support

  • Help
  • Feedback
  • Terms & Privacy

    Connect

  • Email
  • Twitter