[C#] 8.2.1 IEnumerable, IEnumerator 인터페이스

ICollection 인터페이스는 C#에서 제공하는 다양한 컬렉션 클랙스들의 기반이 되는 인터페이스입니다. 그리고 ICollection 인터페이스는 IEnumerable 인터페이스를 기반으로 확장된 인터페이스입니다. C#에서 제공되는 컬렉션 개체가 foreach 구문을 통해 보관된 각 요소에 공통적인 작업을 수행할 수 있는 것도 내부적으로 IEnumerable 인터페이스를 기반으로 정의하였기 때문입니다.

IEnuerable 인터페이스에는 foreach 구문에서 필요한 멤버들을 약속한 IEnumerator 개체를 반환하는 GetEnumerator 메서드를 제공하고 있습니다.

▶ IEnumerable, IEnumerator에 약속한 멤버들

interface IEnumerable
{
    IEnumerator GetEnumerator();
}
interface IEnumerator
{
    bool MoveNext(); //다음 요소 위치로 이동, 더 이상 없으면 false 반환
    void Reset();       //초기 상태로 바꿈
    Object Current    //현재 위치의 요소를 가져오기
    {
        get;
    }
}

간단하게 회원 클래스를 정의하고 회원 개체를 보관하는 컬렉션을 정의해 봅시다.

▶ Member 클래스 정의

class Member
{
    public int Id
    {
        get;
        private set;
    }
    public string Name
    {
        get;
        private set;
    }
    public Member(int id, string name)
    {
        Id = id;
        Name = name;
    }
    public override string ToString()
    {
        return string.Format("아이디:{0} 이름:{1}", Id, Name);
    }
}

▶ Member 개체를 보관하는 MemberCollection 정의

class MemberCollection
{
    ArrayList ar = new ArrayList();
    public void AddMemer(Member member)
    {
        ar.Add(member);
    }
}

이처럼 MemberCollection을 정의하면 IEnumerator를 참조할 수 있는 GetEnumerator 메서드가 없어서 foreach 구문을 이용하여 보관된 요소에 접근하려고 하면 오류가 발생합니다.

foreach 구문 오류 화면
[그림 44] foreach 구문 오류 화면

이번에는 IEnumerable 인터페이스를 기반으로 MemberCollection 클래스를 정의해 봅시다. 물론, IEnumerable 인터페이스에 약속된 GetEnumerator 메서드를 구체적으로 구현해야 합니다. 여기에서는 MemberCollection 내부에 있는 ArraryList에 보관된 요소에 접근할 수 있는 IEnumerator를 반환할게요. 이처럼 IEnuerator 기반으로 MemberCollection을 정의하면 foreach 구문을 사용할 수 있습니다.

▶ IEnumerable 기반의 MemberCollection 정의

class MemberCollection:IEnumerable
{
    ArrayList ar = new ArrayList(); //내부 컬렉션
    IEnumerator IEnumerable.GetEnumerator()//IEnumerable에 약속된 메서드 구현
    {
        return ar.GetEnumerator();//내부 컬렉션을 이용하여 IEnuerator 반환
    }
    public void AddMemer(Member member)
    {
        ar.Add(member);
    }
}

▶ foreach MemberCollection 개체 사용

class Program
{
    static void Main(string[] args)
    {
        MemberCollection mc = new MemberCollection();
        mc.AddMemer(new Member(1,"홍길동"));
        mc.AddMemer(new Member(2,"강감찬"));

        foreach(Member member in mc)
        {
            Console.WriteLine(member.ToString());
        }
    }
}

▶ 실행 결과

아이디:1 이름:홍길동
아이디:2 이름:강감찬

그런데 내부에 분리된 여러 멤버를 갖고 있을 때 foreach 문을 이용하여 멤버들을 열거할 수 있게 하려면 어떻게 해야 할까요? 이럴 때에는 IEnuerator 인터페이스를 기반으로 형식을 정의하여 IEnuerator 인터페이스에 약속된 멤버들을 구현합니다.

구체적인 예를 통해 살펴봅시다.

이번에는 수강생과 강사, 강의실을 정의합시다. 강의실에는 강사 한 명과 여러 명의 수강생이 들어갈 수 있고 foreach 문을 이용하여 강의실에 있는 강사와 수강생을 열거할 수 있게 합시다.

먼저, 간단하게 수강생과 강사를 정의합시다.

▶ 수강생과 강사 정의

class Tutee
{
    public string Name
    {
        get;
        private set;
    }
    public Tutee(string name)
    {
        Name = name;
    }
    public override string ToString()
    {
        return string.Format("수강생 이름:{0}", Name);
    }
}
class Tutor
{
    public string Name
    {
        get;
        private set;
    }
    public Tutor(string name)
    {
        Name = name;
    }
    public override string ToString()
    {
        return string.Format("강사 이름:{0}", Name);
    }
}

강의실은 강사 개체를 관리하는 멤버 필드와 수강생들을 보관하는 컬렉션을 멤버로 두겠습니다. 그리고 강사가 들어오는 메서드와 수강생이 들어오는 메서드를 정의합시다.

class LectureRoom
{
    Tutor tutor = null;
    ArrayList tutees = new ArrayList(); 
    public void AddTutee(Tutee tutee)
    {
        tutees.Add(tutee);
    }
    public bool InTutor(Tutor tutor)
    {
        if (this.tutor == null)
        {
            this.tutor = tutor;
            return true;
        }
        return false;
    }
}

이제 foreach 문을 사용할 수 있게 IEnumerable 인터페이스와 IEnumerator 인터페이스 기반의 형식으로 변경합시다.

class LectureRoom : IEnumerable,IEnumerator

물론, IEnumerable 인터페이스와 IEnumerator 인터페이스에 약속한 멤버를 구체적으로 구현해야겠지요. IEnumerable 인터페이스에 약속된 GetEnumerator는 어떻게 구현하면 될까요? 자기 자신이 IEnumerator이므로 자기 자신을 반환하세요.

IEnumerator IEnumerable.GetEnumerator()
{
    return this;
}

이제 IEnumerator 인터페이스에 약속한 멤버들을 구현합시다.

foreach 구문을 사용하면 MoveNext 메서드를 호출하여 Current 속성으로 요소를 참조하는 것을 반복합니다. 따라서 내부에는 현재 위치를 기억하기 위한 멤버가 필요합니다.

int now;

그리고 foreach 구문을 처음 사용할 때 now값이 초기 상태로 되어야 하므로 강의실 생성자에서 Reset 메서드를 호출할게요.

public LectureRoom()
{
    Reset();
}

foreach 문은 MoveNext를 먼저 수행한 후에 Current 속성으로 요소를 참조하는 것을 반복하므로 Reset 메서드에서는 now의 초기값을 -1로 설정할게요.

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

MoveNext에서는 now를 1 증가시키는 작업을 하겠죠.

now++;

그리고 강사가 없으면 현재 위치인 now가 수강생 수보다 작으면 true를 반환하고 그렇지 않으면 Reset을 호출하여 now를 초기 위치로 변경하고 false를 반환하세요.

if (tutor == null)
{
    if (now < tutees.Count)
    {
        return true;
    }
    Reset();
    return false;
}

강사가 있으면 now가 수강생 수보다 작거나 같을 때 true를 반환하고 그렇지 않으면 Reset을 호출하여 now를 초기 위치로 변경하고 false를 반환합니다.

if (now <= tutees.Count)
{
    return true;    
}
Reset();
return false;

Current 속성에서는 현재 위치의 사람을 반환하면 됩니다. 만약, 강사가 없다면 수강생들을 보관한 tutees 컬렉션의 now 인덱스에 있는 수강생을 반환하면 되겠죠. 그리고 강사가 있고 now가 0이면 강사를 반환합니다. 강사가 있고 now가 0이 아니면 tutees 컬렉션의 now-1 인덱스에 있는 수강생을 반환하세요.

public object Current
{
    get 
    {
        if(tutor == null)
        {
            return tutees[now];
        }
        if (now == 0)
        {
            return tutor;
        }
        return tutees[now - 1];
    }
}

이처럼 내부에 분리되어 있는 멤버를 가지고 있는 개체의 요소들을 foreach 문을 이용하여 열거하고자 하면 IEnuerator 인터페이스 기반의 형식으로 정의하세요.

▶ 강의실 정의 (foreach 문으로 내부에 강사와 수강생을 열거할 수 있음)

class LectureRoom : IEnumerable,IEnumerator
{
    Tutor tutor = null;
    ArrayList tutees = new ArrayList();

    int now; //foreach 문으로 열거할 때의 현재 위치
    public LectureRoom()
    {
        Reset();
    }
    IEnumerator IEnumerable.GetEnumerator()//IEnumerable에 약속된 메서드 구현
    {
        return this;
    }
    public void AddTutee(Tutee tutee)
    {
        tutees.Add(tutee);
    }
    public bool InTutor(Tutor tutor)
    {
        if (this.tutor == null)
        {
            this.tutor = tutor;
            return true;
        }
        return false;
    }

    #region IEnumerator 멤버
    public object Current
    {
        get 
        {
            if(tutor == null)//강사가 없을 때
            {
                return tutees[now];
            }
            //강사가 있을 때
            if (now == 0)
            {
                return tutor;
            }
            return tutees[now - 1];
        }
    }
    public bool MoveNext()
    {
        now++;
        if (tutor == null) //강사가 없을 때
        {
            if (now < tutees.Count)
            {
                return true;
            }
            Reset(); //현재 위치 재설정
            return false;
        }
        //강사가 있을 때
        if (now <= tutees.Count)
        {
            return true;
        }
        Reset(); //현재 위치 재설정
        return false;
    }
    public void Reset()
    {
        now = -1;
    }
    #endregion
}

다음은 foreach 문을 이용하여 강의실에 있는 요소들을 열거하는 예제입니다.

▶ foreach 문을 이용하여 강의실 개체의 요소들을 열거

class Program
{
    static void Main(string[] args)
    {
        LectureRoom lr = new LectureRoom();
        lr.AddTutee(new Tutee("홍길동"));
        lr.AddTutee(new Tutee("강감찬"));

        lr.InTutor(new Tutor("언휴"));
        foreach(object obj in lr)
        {
            Console.WriteLine(obj);
        }
    }
}

▶ 실행 결과

강사 이름:언휴
수강생 이름:홍길동
수강생 이름:강감찬