C# - IEnumerable, IEnumerator

이도희·2023년 6월 29일
0

C#

목록 보기
9/9

참고자료
1. https://learn.microsoft.com/ko-kr/dotnet/api/system.collections.ienumerator?redirectedfrom=MSDN&view=net-8.0
2. https://learn.microsoft.com/ko-kr/dotnet/api/system.collections.ienumerable?view=net-8.0

흔히 사용하는 List, Dictionary 등 Collection에 IEnumerable 인터페이스가 상속되어 있는 것을 확인할 수 있다. IEnumerable과 IEnumerator의 역할을 간단히 이야기해보자면 collection을 순회하기 위한 인터페이스라고 이해할 수 있다. 그럼 하나씩 자세히 살펴보자.

IEnumerable Interface

IEnumerator를 리턴 시키는 Getter의 역할을 하는 인터페이스이다. Collection을 순회할 때 foreach문을 주로 사용하는데 이것을 가능하게 해주는 것이 바로 IEnumerable Interface의 GetEnumerator 함수이다.

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

IEnumerator Interface

컬렉션을 단순하게 반복할 수 있도록 지원한다. 다음의 구성 요소를 가지고 있다.

  • Current : 현재 위치의 데이터 반환
  • MoveNext : 다음 위치로 이동 후 다음 위치에 데이터 존재 여부 반환 (즉, true를 return하면 다음 값을, false를 return하면 종료를 의미)
  • Reset : 인덱스를 초기 위치로 변경
public interface IEnumerator
{
    object? Current { get; }
    bool MoveNext();
    void Reset();
}

구현 예제

다음은 마이크로소프트 공식문서에서 IEnumerable과 IEnumerator 설명에 사용하고 있는 코드이다.

메인 함수

Person을 나타내는 클래스를 정의한다. Main 함수에서는 Person 클래스를 담는 배열을 생성한다. 이를 People이라는 IEnumerable을 상속받은 클래스의 생성자에 넣어 Instantiate한다. foreach를 통해 People의 객체를 순회하며, 이때 GetEnumerator 함수를 통해 IEnumerator 객체를 통해 순회하게 된다.

using System;
using System.Collections;

// Person 클래스 
public class Person
{
    public Person(string fName, string lName)
    {
        this.firstName = fName;
        this.lastName = lName;
    }

    public string firstName;
    public string lastName;
}

class App
{
    static void Main()
    {
    	// Person 배열 
        Person[] peopleArray = new Person[3]
        {
            new Person("John", "Smith"),
            new Person("Jim", "Johnson"),
            new Person("Sue", "Rabon"),
        };
		// IEnumerable 상속받은 People 클래스 인스턴스 생성
        People peopleList = new People(peopleArray);
        foreach (Person p in peopleList) // foreach로 순회 
            Console.WriteLine(p.firstName + " " + p.lastName);
    }
}

/* This code produces output similar to the following:
 *
 * John Smith
 * Jim Johnson
 * Sue Rabon
 *
 */

IEnumerator

IEnumerator를 상속받은 PeopleEnum 클래스이다. 현재 위치의 index를 나타내기 위해 position이라는 int 값을 정의한다.

  • Current : 배열에 현재 position값의 Person을 찾아와 반환한다. (Person을 object로 형변환하여 return하게 된다.)
  • MoveNext : position을 한 칸 이동 후 position과 현재 배열 길이를 비교해 끝에 도달했는 지 아닌지에 대한 여부를 반환한다.
  • Reset : position을 다시 첫 번째 요소 이전의 index인 -1로 변경한다.

// When you implement IEnumerable, you must also implement IEnumerator.
public class PeopleEnum : IEnumerator
{
    public Person[] _people;

    // Enumerators are positioned before the first element
    // until the first MoveNext() call.
    int position = -1;
	// PeopleEnum 생성자
    public PeopleEnum(Person[] list)
    {
        _people = list;
    }
	// 다음 요소로 이동 및 끝인지 아닌지 여부 반환
    public bool MoveNext()
    {
        position++;
        return (position < _people.Length);
    }
	// 처음 요소 이전의 index로 초기화
    public void Reset()
    {
        position = -1;
    }
	// IEnumerator Current 프로퍼티 구현 
    object IEnumerator.Current
    {
        get
        {
            return Current;
        }
    }
	// Person 배열에서 현재 position으로 접근해서 해당하는 Person 반환
    public Person Current
    {
        get
        {
            try
            {
                return _people[position];
            }
            catch (IndexOutOfRangeException)
            {
                throw new InvalidOperationException();
            }
        }
    }
}

IEnumerable

IEnumerable을 상속받은 People 클래스이다. 생성자로 받은 Person 배열을 통해 데이터를 초기화해준다. GetEnumerator 함수에서는 IEnumerator를 상속받은 PeopleEnum을 Instantiate해서 반환해준다.

// Collection of Person objects. This class
// implements IEnumerable so that it can be used
// with ForEach syntax.
public class People : IEnumerable
{
    private Person[] _people;
    // People 클래스의 생성자 -> Person 배열 초기화 
    public People(Person[] pArray)
    {
        _people = new Person[pArray.Length];

        for (int i = 0; i < pArray.Length; i++)
        {
            _people[i] = pArray[i];
        }
    }

	// IEnumerable interface 함수
    IEnumerator IEnumerable.GetEnumerator()
    {
       return (IEnumerator) GetEnumerator();
    }
	// PeopleEnum이라는 IEnumerator를 반환하는 함수 
    public PeopleEnum GetEnumerator()
    {
        return new PeopleEnum(_people);
    }
}

왜 IEnumerable과 IEnumerator 둘 다 사용해야하는가?

IEnumerable과 IEnumerator의 구조를 간단히 도식화해보면 다음과 같다. 굉장히 단순한 의문이 들 수 있는데 결국 IEnumerable도 IEnumerator를 내부적으로 사용하므로 두 인터페이스 다 동일한 결과를 내는데 왜 굳이 IEnumerable을 거치는 식으로 구현할까?

예를 들어 다음과 같이 구현한다면 같은 결과를 낼 수 있다. month를 string으로 들고 있는 month 리스트가 있다고 가정하자.

// IEnumerable
IEnumerable<string> monthWithIEnumerable = (IEnumerable<string>)month;
foreach(string currentMonth in monthWithIEnumerable)
{
   Console.WriteLine(currentMonth);
}

// IEnumerator
IEnumerator<string> monthWithIEnumerator = month.GetEnumerator()
while(iEnumeratorOfString.MoveNext())
{
	Console.WriteLine(monthWithIEnumerator.Current);
}

보면 IEnumerable의 경우 foreach를 통해 구현하는 반면 IEnumerator는 MoveNext 함수와 Current 프로퍼티에 접근해야하는 등 좀 더 복잡한 편이다. 그래서 하나의 이유로 들 수 있는 것은 구문을 더 짧고 간단히 만들 수 있기 때문이라고 할 수 있다.

좀 더 근본적으로 IEnumerable과 IEnumerator의 역할에 대해 생각해보자. 두 개의 인터페이스가 본질적으로 의미하고 있는 것이 다르다. IEnumerable은 일종의 "요소들을 순회할 수 있는가?"에 초점을 맞춘다면 IEnumerator은 "어떻게 요소들을 순회할 것인가?"에 초점을 두고 있다. (여러 종류의 Collection이 있는 상태에서 모두 순회하는 방식이 같다면 IEnumerator를 각각에 맞춰 정의할 필요가 없이 하나의 IEnumerator를 구현하면 된다.) 정리하면 두 인터페이스를 상속함으로써 가지는 의미가 다르다. 그래서 두 인터페이스가 따로 정의된 것이 또다른 이유라고 생각된다.

profile
하나씩 심어 나가는 개발 농장🥕 (블로그 이전중)

0개의 댓글