[C#] 47. IEnumerable와 IEnumerator, 그리고 yield 키워드


Study/C#  2021. 10. 11. 17:43

안녕하세요. 명월입니다.


이 글은 IEnumerable와 IEnumerator, 그리고 yield 키워드에 대한 글입니다.


이전에 제가 Linq 식을 설명하면서 IEnumerable에 대해서 간단하게 설명한 적이 있습니다.

링크 - [C#] 30. 제네릭(Generic) 사용법


IEnumerable의 인터페이스는 반복자 패턴과 관계 있는 패턴으로 우리가 반복문 키워드 foreach에서 사용되는 인터페이스입니다.

링크 - [Design pattern] 반복자 패턴 (Iterator pattern)


IEnumerable의 인터페이스는 GetEnumerator 함수가 정의되어 있어서 IEnumerator로 반환을 하게 됩니다.

그리고 IEnumerator의 경우는 foreach에서 사용될 패턴의 동작 인터페이스로 현재 값에 대한 프로퍼티 Current, 포인트 이동과 값이 존재하는 지에 대한 함수 MoveNext, 그리고 포인터의 초기화를 하게 되는 함수 Reset로 이루어져 있습니다.

using System;
using System.Collections;

namespace Example
{
  // IEnumerable를 상속 받은 List
  class List : IEnumerable
  {
    // IEnumerator를 상속 받은 Node 
    private class Node : IEnumerator
    {
      // 데이터를 1부터 10까지의 배열
      private int[] data = new int[]
      {
        1,2,3,4,5,6,7,8,9,10
      };
      // 현제 위치 포지션
      private int pos = 0;
      // 현재 포지션 값 더하기
      public object Current
      {
        get
        {
          // pos-1를 취득한다.
          return data[pos - 1];
        }
      }
      // 포지션 이동
      public bool MoveNext()
      {
        // 포지션이 배열 크기를 넘어서면 false
        if (pos >= data.Length)
        {
          return false;
        }
        // 포지션 이동
        pos++;
        // true
        return true;
      }
      // 포지션 위치 초기화
      public void Reset()
      {
        // 포지션 이동 0
        pos = 0;
      }
    }
    // 인스턴스 생성
    private IEnumerator node = new Node();
    // GetEnumerator를 리턴
    public IEnumerator GetEnumerator()
    {
      node.Reset();
      return node;
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 리스트 인스턴스 생성
      var list = new List();
      // 반복문으로 list의 GetEnumerator 함수를 통해서 리스트 형식으로 가져온다.
      foreach (int item in list)
      {
        // MoveNext와 Current를 통해서 값을 콘솔에 표시
        Console.WriteLine(item);
      }
      
      // 반복문으로 list의 GetEnumerator 함수를 통해서 리스트 형식으로 가져온다.
      foreach (int item in list)
      {
        // MoveNext와 Current를 통해서 값을 콘솔에 표시
        Console.WriteLine(item);
      }
      
      // 아무 키나 누르면 종료
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}

foeach의 반복문 키워드는 IEnumerable의 인스턴스를 상속받은 클래스 값을 사용할 수 있습니다. 우리가 보통 List나 Array를 foreach에 넣어서 사용하게 되는데, 이 클래스가 IEnumerable의 인스턴스를 상속받은 것과 같은 의미입니다.

IEnumerable의 인터페이스는 GetEnumerator 함수가 정의되어 있고 IEnumerator의 인터페이스 상속받은 클래스를 반환합니다.

IEnumerator의 인터페이스에서는 foreach에서 사직을 하게 되면 Reset 함수를 호출하여 포인터를 초기화 하고 foreach에서 다음 포인터로 넘어갈 때마다 MoveNext의 함수를 호출하고 item으로 데이터를 넘길 때면 Current 프로퍼티를 사용합니다.


여기서 조금 이상한 코드가 보이는데 MoveNext가 먼저 호출이 되고 Current를 호출이 되는데 MoveNext의 반환값은 현재값이 null인지 아닌 지에 대한 체크하고 포인터를 이동합니다.

즉, Current에서는 pos - 1로 리턴하고 MoveNext 함수에서는 현재 포인터의 대한 값의 null 여부를 체크하기 때문에 return 값은 pos >= data.Lengt인지 확인하고 pos++로 위치를 이동하게 됩니다.


여기서 우리는 yield 키워드를 사용하면 IEnumerator를 좀 더 쉽게 만들 수 있습니다.

using System;
using System.Collections;

namespace Example
{
  // IEnumerable를 상속 받은 List
  class List : IEnumerable
  {
    // 데이터를 1부터 10까지의 배열
    private int[] data = new int[]
    {
      1,2,3,4,5,6,7,8,9,10
    };
    // GetEnumerator를 리턴
    public IEnumerator GetEnumerator()
    {
      // yield 키워드를 통해서 위 data 배열이 순서대로 return 된다.
      yield return data[0];
      yield return data[1];
      yield return data[2];
      yield return data[3];
      yield return data[4];
      yield return data[5];
      yield return data[6];
      yield return data[7];
      yield return data[8];
      yield return data[9];
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 리스트 인스턴스 생성
      var list = new List();
      // 반복문으로 list의 GetEnumerator 함수를 통해서 리스트 형식으로 가져온다.
      foreach (int item in list)
      {
        // MoveNext와 Current를 통해서 값을 콘솔에 표시
        Console.WriteLine(item);
      }

      // 반복문으로 list의 GetEnumerator 함수를 통해서 리스트 형식으로 가져온다.
      foreach (int item in list)
      {
        // MoveNext와 Current를 통해서 값을 콘솔에 표시
        Console.WriteLine(item);
      }

      // 아무 키나 누르면 종료
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}

IEnumerator 인터페이스를 상속받은 클래스와 같은 결과의 값이 표시됩니다.

yield의 경우는 호출될 때마다 return의 값을 다르게 할 수 있습니다. 즉, GetEnumerator() 호출이 되면 yield 키워드를 파악해서 하나의 함수로 되어 있는 연결리스트를 생성합니다.

그리고 MoveNext가 호출이 될 때마다 다음 단계의 yield까지 실행이 되는 방법입니다.


또, IEnumerator 인터페이스는 foreach에서만 사용하는 게 아니고 Linq에서도 사용가능합니다.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // IEnumerable를 상속 받은 List
  class List : IEnumerable<int>
  {
    // 데이터를 1부터 10까지의 배열
    private int[] data = new int[]
    {
      1,2,3,4,5,6,7,8,9,10
    };
    // GetEnumerator를 리턴 (IEnumerable<int> 인터페이스)
    public IEnumerator<int> GetEnumerator()
    {
      // 데이터의 개수만큼
      yield return data[0];
      yield return data[1];
      yield return data[2];
      yield return data[3];
      yield return data[4];
      yield return data[5];
      yield return data[6];
      yield return data[7];
      yield return data[8];
      yield return data[9];
    }
    // GetEnumerator를 리턴 (IEnumerable 인터페이스)
    IEnumerator IEnumerable.GetEnumerator()
    {
      // 오버로딩으로 인한 두 함수가 나누어져 있기 때문에 위 함수를 호출하면 됩니다.
      return GetEnumerator();
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 리스트 인스턴스 생성
      var list = new List();
      // 위 yield으로 되어 있는 리스트를 Linq 식으로 리스트로 만들 수 있다
      var ret = list.Where(x => x > 5).OrderByDescending(x => x);
      // 결과를 foreach
      foreach (var item in ret)
      {
        // 콘솔 출력
        Console.WriteLine(item);
      }

      // 아무 키나 누르면 종료
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}

사실 yield 키워드는 자주 사용하지 않습니다. 저도 문법은 알고 있지만 상당히 생소한 문법입니다. 물론 개발하는 사람에 따라 다르기는 하지만, 저의 경우는 다른 언어(?)에는 없는 문법이고 굳이 익숙하지 않은 문법으로 가독성을 안좋게 할 이유는 없다고 생각합니다.

사실 보통 IEnumerable나 IEnumerator를 상속받아서 개발하는 것 자체가 거의 별로 없습니다. 상황에 따라 캐쉬 알고리즘을 만든다거나 List 알고리즘을 더 효율적으로 개선할 수 있겠지만, .Net Framework에서 제공하는 List 알고리즘 자체가 꽤 우수하고 새로운 알고리즘을 만드는 것으로 버그에 대한 안정성을 보장할 수 없어서 입니다.

Array도 있고 그렇다고 List를 쓴다고 시스템이 그렇게 느려질 것 같지도 않습니다.


여기까지 IEnumerable와 IEnumerator, 그리고 yield 키워드에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.