Iterator 트레이트의 연관 타입(associated type)

아래 코드의 핵심은 Iterator 트레이트의 연관 타입(associated type)트레이트 구현에 의한 타입 유추입니다. Item이 직접적으로 코드 안에서 보이지 않지만, 트레이트 시스템이 자동으로 Iterator trait의 type Item과 연결해 주고 있습니다.

1. 예제 코드

struct GivesOne;

impl Iterator for GivesOne {
    type Item = i32;
    fn next(&mut self) -> Option<i32> {
        Some(1)
    }

}

fn main() {
    let five_ones = GivesOne.into_iter().take(5).collect::<Vec<i32>>();

    println!("{five_ones:?}");
}

2. 구조체 정의

struct GivesOne;
  • GivesOne은 단순한 빈 구조체입니다.

3.Iterator 트레이트 구현

impl Iterator for GivesOne {
    type Item = i32;

    fn next(&mut self) -> Option<i32> {
        Some(1)
    }
}

여기서 중요한 부분이 바로 type Item = i32; 입니다.
Iterator 트레이트는 아래와 같이 정의되어 있습니다 (표준 라이브러리 축약본).

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

즉,

  • Item은 연관 타입으로, 이 이터레이터가 어떤 타입의 요소를 내보내는지를 지정합니다.
  • Self::Item이 Option<Self::Item> 안에서 쓰이죠.

    그런데 GivesOne에서는 type Item = i32;로 정했으니,
    → Self::Item이 i32가 됩니다.
    → 따라서 fn next의 실제 타입은 fn next(&mut self) -> Option<i32>가 되는 겁니다.

next()는다음 요소를 하나 반환하거나(None을 반환하면 종료) 하는 메서드입니다.
이터레이터는 내부 상태를 가지고 “이번에 무엇을 내보낼지”를 결정하죠.

GivesOne 구조체에는 아무 필드도 없습니다. 즉, 몇 번째 요소를 내보냈는지 추적하는 정보가 없습니다. 따라서, 이터레이터는 “항상 똑같은 값 1만 계속 내보내는” 무한 반복 이터레이터가 됩니다.

4. GivesOne.into_iter()의 동작

let five_ones = GivesOne.into_iter().take(5).collect::<Vec<i32>>();

여기서 헷갈릴 수 있는 부분은 into_iter()인데,
GivesOne이 스스로 Iterator를 구현하고 있기 때문에,
Rust는 GivesOne.into_iter()를 IntoIterator 트레이트를 통해 다음처럼 해석합니다.

impl<I: Iterator> IntoIterator for I {
    type Item = I::Item;
    type IntoIter = I;

    fn into_iter(self) -> I {
        self
    }
}

즉,

  • GivesOne이 이미 Iterator니까
    → IntoIterator도 자동으로 적용되고,
    → into_iter()는 그냥 자기 자신(GivesOne)을 반환합니다.

5. 체인 전체의 타입 흐름

  1. GivesOne.into_iter() → 타입: GivesOne
    위에서 알아본 바와 같이 next() 메소드는 영원히 Some(1)만 반환하고, None을 반환하지 않으므로 무한루프가 됩니다.
  2. .take(5) → 타입: Take<GivesOne>
    따라서.take(5)로 “앞의 5번만 가져오라”고 제한을 두는 것입니다.
  3. collect::<Vec<i32>>()
    collect()는 Item 타입을 기반으로 Vec<i32>를 만듭니다.

      6. 실제로 내부 타입이 어떻게 “보이지 않지만 작동하는가?”

      Rust는 트레이트 바인딩을 통해 자동으로 타입을 유추합니다.
      Item은 직접 변수로 존재하지 않고, 트레이트 정의 안에서 연관 타입으로만 쓰이죠.

      즉,
      GivesOne → Iterator → Item = i32
      이 연결이 트레이트 시스템 레벨에서 자동으로 이어지므로,
      어디에도 Item이라는 이름이 코드에 명시적으로 안 나와도,
      Rust는 Iterator<Item = i32>인 것을 정확히 압니다.

      7. 요약

      코드 부분역할
      type Item = i32;이터레이터가 내보내는 값의 타입을 지정
      fn next(&mut self)실제로 값을 하나씩 생성
      into_iter()Iterator를 자기 자신으로 반환
      take(5)최대 5번 next()호출
      collect::<Vec<i32>>()결과를 Vec<i32>로 수집

      결과적으로println!(“{five_ones:?}”)는 [1, 1, 1, 1, 1]을 출력하게 됩니다.

      Iterator 실행 결과

      Rust의 특별한 용어 정리

      Rust에는 소유권, 참조, 라이프타임 등 고유한 용어들이 있는데 이외에도 생소하거나 중요한 용어인 variant, field, pattern, match arm, block, scope, associated type, attribute에 대해서 살펴 보겠습니다.

      1. variant

      • 정의: enum에서 각각의 경우(상태/종류)를 의미.
      • 예시:
      enum Color {
      Red,
      Blue,
      Green,
      }
      let c = Color::Red; // Red는 Color 타입의 variant

      여기서 Red, Blue, Green이 각각 variant이다.

      2. field

      • 정의: struct(또는 enum의 variant)에 속하는 개별 데이터 항목.
      • 예시:
      struct Point {
      x: i32, // field
      y: i32, // field
      }
      let p = Point { x: 1, y: 2 };

      x와 y가 각각 field이고, i32는 각각의 field의 타입(type of the field 또는 필드형/필드 타입)입니다.

      enum Message {
      Quit, // 필드 없음
      Move { x: i32, y: i32 }, // 필드 x, y가 있는 구조체 스타일
      Write(String), // 이름 없는 튜플 스타일의 필드 String
      ChangeColor(i32, i32, i32), // 이름 없는 튜플 스타일의 필드 i32
      }

      Message enum의 variant가 갖는 값 또는 구조가 곧 해당 variant의 “필드”입니다. 구조체 스타일인 경우는 필드 개념이 동일하며, 튜플 형식인 경우는 구조체와 달리 필드가 없지만 값이 필드가 됩니다.

      enum은 이 필드(데이터) 덕분에, variant별로 타입에 따라 다양한 정보를 유연하게 표현할 수 있습니다.

      3. pattern

      • 정의: 값을 구조적으로 분해 처리하기 위한 형태. match, let, 함수 매개 변수 등에서 사용합니다.
      • 예시:
      // struct Point 정의
      struct Point {
      x: i32,
      y: i32,
      }

      fn main() {
      // 튜플 패턴 예제
      let (a, b) = (1, 2);
      println!("a = {}, b = {}", a, b);

      // 구조체 및 구조체 패턴 매칭 예제
      let p = Point { x: 10, y: 20 };
      match p {
      Point { x, y } => println!("({}, {})", x, y),
      }
      }

      패턴을 이용해 복잡한 데이터를 쉽게 분해할 수 있다.

      • let (a, b) = (1, 2); => 오른쪽 (1, 2)는 타입이 (i32, i32)인 튜플이며, 이 값을 튜플로 받아서, 첫 번째 요소는 변수 a에, 두 번째 요소는 변수 b에 바인딩해줘”라는 뜻입니다.
      • match p {
        Point { x, y } => println!(“({}, {})”, x, y),
        } 에서
        Point { x, y }는 매칭 대상(p)이 해당 구조와 일치하는지 검사하고, 일치한다면 그 필드값을 변수로 분해해주는 패턴 역할을 합니다. 따라서 match에서 구조체 내부 값을 분해하고 싶으면 항상 이런 형태의 패턴을 사용하게 됩니다.

      4. match arm

      • 정의: match 구문의 각분기(패턴 + 처리 블록).
      • 예시:
      let n = 3;
      match n {
      1 => println!("One"), // arm 1
      2 | 3 | 5 => println!("Prime"), // arm 2
      _ => println!("Other"), // arm 3
      }

      각각이 match arm이며, 패턴과 실행할 코드 블록으로 구성되어 있다.

      match arm에서의 패턴(pattern)은, 매칭 대상이 되는 값이 어떤 구조나 값을 가지고 있는지 비교하고, 해당 구조와 일치하면 그 arm의 코드를 실행하도록 하는 역할을 합니다.
      즉, match 구문의 각 arm(갈래, 분기)은 패턴 => 실행코드 형태로 이루어지며, 패턴은 값의 형태를 설명하거나 내부 값을 분해하는 구조입니다

      4. block

      • 정의: 중괄호 {}로 둘러싸인 코드 구역. 블록은 표현식이며 값과 타입을 가진다.
      • 예시:
      let result = {
      let x = 2;
      x * x // 마지막 표현식이 결과값이 됨
      };
      // result = 4

      블록 내에서 선언된 변수는 해당 블록에서만 유효하다.

      5. scope

      • 정의: 변수나 아이템이 유효한 코드의 범위.
      • 예시:
      fn main() {
      let x = 10; // x는 main 함수 블록(scope)에서만 유효
      }

      scope이 끝나면 변수는 더 이상 쓸 수 없다.

      가. block과 scope 비교

      (1) block

      • 코드에서 중괄호 {}로 둘러싸인 부분 자체를 block이라고 합니다.
      • 예시:rust{ let a = 1; println!("{a}"); }
      • 이 부분 전체가 block입니다.

      (2) scope

      • scope는 어떤 변수(또는 아이템)를 ‘볼 수 있고 사용할 수 있는 코드의 범위’입니다.
      • scope는 보통 block에 의해 결정되지만, 완전히 같지는 않습니다.
        • 모든 block은 새로운 scope를 열지만,
        • scope의 개념은 block 외에도 함수, 모듈, crate 등 더 넓거나 좁게 적용될 수 있습니다.

      (3) 차이점 및 예제

      • block: { ... }로 감싸진 모든 코드 덩어리를 의미.
      • scope: 그 안에서 선언된 변수나 아이템이 유효한 코드의 범위.
      • 모든 block이 scope를 정의하지만, scope는 더 넓은 개념입니다.

      예시:

      fn main() {                      // main 함수 block, 여기가 scope 시작
      let outer = 10; // 'outer'의 scope는 main 함수 전체

      { // 새로운 block 시작, 이 안이 block scope
      let inner = 20; // 'inner'의 scope는 이 중괄호 안
      println!("{}, {}", outer, inner);
      } // inner는 여기서 scope 종료

      println!("{}", outer); // ok
      println!("{}", inner); // 에러, inner는 scope out!
      }

      여기서 inner는 작은 block(scope)에만 존재.
      outer는 main block(scope) 전체에 존재.

      6. associated type

      • 정의: 트레잇에서 타입 매개변수 대신 트레잇에 연결된 타입을 선언.
      • 예시:
      trait Iterator {
      type Item; // associated type

      fn next(&mut self) -> Option<Self::Item>;
      }

      struct Counter;
      impl Iterator for Counter {
      type Item = i32;
      fn next(&mut self) -> Option<i32> { /* 구현 */ None }
      }

      Iterator 트레잇에서 Item이 associated type이다.

      가. 타입 매개변수와 associated type(연관 타입)의 정의

      (1) 타입 매개변수:

      • generic에서 사용하는 타입 변수입니다.
      • 예: trait MyTrait<T> { … }에서 T가 타입 매개변수입니다.

      (2) associated type(연관 타입)

      • 트레잇에서 선언해서, 트레잇을 구현할 때 구체적으로 지정하는 타입입니다.
      • trait Iterator { type Item; … }에서 Item이 이에 해당.

      두 가지의 차이를 간단히 예시로 정리하면:

      방법선언 방식사용 예시주의점
      타입 매개변수trait MyTrait<T> { … }struct Foo;
      impl MyTrait<u32> for Foo { … }
      사용 때마다 타입 지정 필요
      associated typetrait MyTrait { type Output; … }struct Bar;
      impl MyTrait for Bar { type Output = u32; }
      트레잇 구현이 타입 결정

      나. 예시

      (1) 타입 매개변수(Generic parameter)를 사용하는 트레잇

      // 타입 매개변수를 사용하는 트레잇
      trait MyTrait<T> {
      fn foo(&self, x: T);
      }

      // 해당 트레잇을 구현하는 타입 예시
      struct MyType;

      impl MyTrait<i32> for MyType {
      fn foo(&self, x: i32) {
      println!("MyTrait<i32>::foo called with x = {}", x);
      }
      }

      fn main() {
      let t = MyType;
      t.foo(100);
      }

      실행 결과:

      MyTrait<i32>::foo called with x = 100

      (2) Associated Type(연관 타입)을 사용하는 트레잇 예시

      // 연관 타입을 사용하는 트레잇
      trait AnotherTrait {
      type Output;
      fn bar(&self) -> Self::Output;
      }

      // 해당 트레잇을 구현하는 타입 예시
      struct MyType;

      impl AnotherTrait for MyType {
      type Output = i32;
      fn bar(&self) -> i32 {
      42
      }
      }

      fn main() {
      let t = MyType;
      let v: i32 = t.bar();
      println!("AnotherTrait::bar returned {}", v);
      }

      실행 결과:

      AnotherTrait::bar returned 42

      7. attribute

      • 정의: 컴파일러에게 부가 정보를 주는 메타데이터. #[…] 또는 #![…] 형태.
      • 예시:
      #[derive(Debug)]
      struct MyStruct;

      #[cfg(target_os = "linux")]
      fn linux_only() {}

      #[allow(dead_code)]
      fn unused_function() {}

      주로 코드 자동 생성(derive), 조건부 컴파일(cfg), 경고 제어 등에 사용된다.

      attribute에 대해서는 다음 편에서 더 자세히 다룰 예정입니다.