본문 바로가기
개발/Unity 내일배움캠프 TIL

C# Enumerable 인터페이스

by 석시 2023. 8. 30.



C#을 만지다보면 종종 등장하던 IEnumerable.

인덱서를 이용해 인덱스를 오버라이딩할 줄은 알았는데, foreach는 오버라이딩이 안되나? 라는 궁금증을 가지고 있었는데, 그 정답이 IEnumerable이었다.

이 김에 정리해보자.

Enumerable vs IEnumerable?

들어가기 전에 확실히 할 부분이, System.LinqEnumerable 클래스에 대해 이야기 하는 것이 아니다.

Enumerable 클래스는 열거형(Enumerable) 컬렉션에 사용가능한 메서드를 모아놓은 클래스라고 생각하면 된다.

즉, 기존의 열거형 컨테이너였던 ArrayList는 물론이거니와, IEnumerable<T>를 구현해놓은 모든 object에 접목하여 사용할 수 있다.

직접 열거형에 대한 함수를 구현할 일은 잘 없고, 내가 만든 클래스를 열거형으로 만드는 경우가 많기 때문에

IEnumerable 인터페이스를 다룰 일이 훨씬 많을 것이다.

IEnumerable 기본 구조

IEnumerable의 코드는 이렇게 되어 있다.

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

너무 단순해서 당황했다.

알고보니 생각한 기능들은 전부 IEnumerator(열거자)에 있었다.

public interface IEnumerator
{
    object? Current { get; }
    bool MoveNext();
    void Reset();
}

Current는 현재 위치의 개체를 반환한다.

MoveNext는 다음 인덱스가 유효한지 아닌지를 반환한다. 유효할 경우 다음 위치로 이동한다.

Reset은 Enumerator를 초기 위치, 즉 처음으로 다시 돌린다.

IEnumerable 상속 예시

using System;
using System.Collections;

// Simple business object.
public class Person
{
    public Person(string fName, string lName)
    {
        this.firstName = fName;
        this.lastName = lName;
    }

    public string firstName;
    public string lastName;
}

// Collection of Person objects. This class
// implements IEnumerable so that it can be used
// with ForEach syntax.
public class People : IEnumerable
{
    private Person[] _people;
    public People(Person[] pArray)
    {
        _people = new Person[pArray.Length];

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

// Implementation for the GetEnumerator method.
    IEnumerator IEnumerable.GetEnumerator()
    {
       return (IEnumerator) GetEnumerator();
    }

    public PeopleEnum GetEnumerator()
    {
        return new PeopleEnum(_people);
    }
}

PeopleEnum이라는 Enumerator는 바로 다음에 있다.

원래 IEnumerable명시적 구현이 되어 있기 때문에 이를 외부에서 호출하려면 객체를 인터페이스 타입으로 업캐스팅해서 호출해줘야 한다.

IEnumerableGetEnumerator()IEnumerator를 반환해줘야하니 PeopleEnumIEnumerator로 업캐스팅해서 반환한다.

명시적 암시적에 관한 내용은 다음의 좋은 글이 있으니 참고하자.

[C#] 암묵적 구현과 명시적 구현
클래스가 인터페이스를 상속하는 경우 인터페이스가 가지는 메소드들을 구현해야 합니다. 이때, 구현하는 방법은 암묵적 구현과 명시적 구현이 있습니다. 간단히 말하자면, 암묵적 구현은 public으로 구현하는 방식이고, 명시적 구현은 접근 한정자를 지정해주지 않고 [void 인터페이스명.메소드명] 형식으로 메소드를 구현하는 방식입니다. 이제 한번 암묵적 구현과 명시적 구현에 대해 알아보겠습니다! 암묵적 구현 public을 사용해서 구현하는 방식을 암묵적 구현이라고 합니다. 암묵적 구현은 외부에서 객체를 통해 쉽게 접근이 가능합니다. 명시적 구현 [void 인터페이스명.메소드명]으로 구현하는 방식을 명시적 구현이라고 합니다. private 하기 때문에 외부에서 객체를 호출 할 수 없습니다. 메소드 뿐만 아니라 ..
https://end-of-code.tistory.com/entry/C-암묵적-구현과-명시적-구현

// 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;

    public PeopleEnum(Person[] list)
    {
        _people = list;
    }

    public bool MoveNext()
    {
        position++;
        return (position < _people.Length);
    }

    public void Reset()
    {
        position = -1;
    }

    object IEnumerator.Current
    {
        get
        {
            return Current;
        }
    }

    public Person Current
    {
        get
        {
            try
            {
                return _people[position];
            }
            catch (IndexOutOfRangeException)
            {
                throw new InvalidOperationException();
            }
        }
    }
}

IEnumerator를 상속받는 부분에서는

Current, MoveNext, Reset에 대한 부분을 모두 구현해줘야 한다.

foreach

IEnumerable을 상속받은 객체는 foreach로 순회가 가능하다!

IEnumerable을 쓰는 가장 큰 이유가 아닐까.

People peopleList = new People(peopleArray);
foreach (Person p in peopleList)
    Console.WriteLine(p.firstName + " " + p.lastName);

yield

사실 IEnumerable 상속을 매우 간단하게 구현할 수 있다.

바로 yield이다.

yield는 비동기 방식의 함수 호출을 위해 사용하는 문으로, 함수 호출자(Caller)에게 컬렉션의 요소를 하나씩 반환할 때 사용한다.

yield return을 하게 되면 함수의 실행이 완전 끝나는 것이 아니라 그 함수를 다시 실행하게 되면 마지막으로 return했던 장소로 돌아와 다시 함수 구문을 실행한다.

foreach (int i in ProduceEvenNumbers(9))
{
    Console.Write(i);
    Console.Write(" ");
}
// Output: 0 2 4 6 8

IEnumerable<int> ProduceEvenNumbers(int upto)
{
    for (int i = 0; i <= upto; i += 2)
    {
        yield return i;
    }
}

반복문과 같이 사용하기 때문에 반복 종료를 명시적으로 해줄 수도 있다.

바로 yield break이다.

Console.WriteLine(string.Join(" ", TakeWhilePositive(new[] { 2, 3, 4, 5, -1, 3, 4})));
// Output: 2 3 4 5

Console.WriteLine(string.Join(" ", TakeWhilePositive(new[] { 9, 8, 7 })));
// Output: 9 8 7

IEnumerable<int> TakeWhilePositive(IEnumerable<int> numbers)
{
    foreach (int n in numbers)
    {
        if (n > 0)
        {
            yield return n;
        }
        else
        {
            yield break;
        }
    }
}

yield return이 작동할 때, 처리를 위해 컴파일러는 자동으로 IEnumerableIEnumerator 클래스를 자동으로 생성한다!

이 때문에 yield를 사용하면 따로 IEnumerator를 구현할 필요가 없는 것이다.

yield의 강점

yield가 좋은 점은 컬렉션의 요소를 일시에 return하지 않는다는 점에서 온다.

따라서 해당 상황에 강점을 가진다.

  • 컬렉션의 데이터 양이 매우 큰 경우
  • 메서드가 무제한의 데이터를 반환할 경우
  • 데이터 하나하나를 계산하는데 걸리는 시간이 너무 오래걸리는 경우


Uploaded by N2T

'개발 > Unity 내일배움캠프 TIL' 카테고리의 다른 글

C# 패턴 일치 (Pattern Matching)  (0) 2023.09.01
C# 중첩 클래스와 partial 클래스  (0) 2023.08.31
C# Action으로 종속성 없애기..?  (0) 2023.08.29
C# StringBuilder 정리  (0) 2023.08.28
C# LINQ 간단 정리  (0) 2023.08.25