프로그래밍/C#

[C#] 열거자 (Enumerator)

갓똥 2020. 4. 26. 16:31
728x90
반응형

1. 문제

using System;
using System.Collections.Generic;

class Program {
    static void Main() {
         int[] arr = {1, 2, 3, 4, 5};
         
         List<int> c1 = new List<int>(arr);
         
         for(int i=0; i < c1.Count; i++) {
             Console.WriteLine(c1[i]);
         }
    }
}

//-----------------------------------------------------

using System;
using System.Collections.Generic;

class Program {
    static void Main() {
         int[] arr = {1, 2, 3, 4, 5};
         
         LinkedList<int> c1 = new LinkedList<int>(arr);
         
         for(int i=0; i < c1.Count; i++) {
             Console.WriteLine(c1[i]); // error
         }
    }
}

 

List를 배열로 초기화하고 요소를 출력해보는 간단한 코드이다.
위에서 List는 연속된 메모리에 요소가 놓이게 되는게 메모리 공간을 떨어뜨려 놓고 싶어졌다고 해보자.
이럴 땐 LinkedList라는게 있어 이걸 사용하면 요소별로 메모리가 따로 떨어지게 된다.
하지만 문제는 c1[i]와 같이 인덱서를 제공하지 않아 출력을 못한다는 문제가 있다.
인덱서를 제공하고 List -> LinkedList만 바꾸면 된다면
바꿔가면서 성능을 비교해보면 좋을텐데 안된다는게 문제이다.
이 때, 반복자 패턴이란 것이 있다.

 ① Collection과 인덱서(indexer)

    => IList<T> 인터페이스를 구현한 컬렉션은 인덱서를 제공

    => LinkedList<T>는 인덱서를 제공 안함

 

 ② 반복자(iterator) 패턴

    => 복합객체의 내부 구조에 상관 없이 동일한 방식의 요소를 열거하는 디자인 패턴

    => Collection 의 내부 구조에 상관없이 동일한 방법으로 요소를 열거

    => C#에서는 반복자(iterator) 대신 열거자(enumerator)라는 용어를 사용

 


2. 해결

using System;
using System.Collections.Generic;

class Program {
    static void Main() {
        int[] arr = {1, 2, 3, 4, 5};
        
        List<int>       c1 = new List<int>(arr);
        LinkedList<int> c2 = new LinkedList<int>(arr);
        
        
    }
}

 

C#에서 모든 컬렉션은 IEnumerable<T> 인터페이스를 구현한다.
이 얘기는 공통의 어떠한 메소드가 있을 것이란 이야기다.
이 메소드는 GetEnumerator()이다.
메소드가 반환하는 것을 열거자라고 부르는데 이것은 객체이다. 
객체가 요소를 가리키며 다음 요소로 이동하거나 값을 꺼내올 수 있게 된다.
또, 모든 열거자의 사용법은 동일하다.
왜냐하면 IEnumerator<T> 인터페이스를 구현하기 때문이다.
여기서 인터페이스 이름을 헷갈릴 수 있는데 IEnumerable<T>은 컬렉션의 인터페이스이고
IEnumerator<T>는 열거자의 인터페이스이다.
따라서 반환값을 받을 때 IEnumerator<T>로 받으면 된다.
요즘은 var를 많이 사용한다. (여기서는 IEnumerator<T>를 사용한다.)

using System;
using System.Collections.Generic;

class Program {
    static void Main() {
        int[] arr = {1, 2, 3, 4, 5};
        
        List<int>       c1 = new List<int>(arr);
        LinkedList<int> c2 = new LinkedList<int>(arr);
        
        IEnumerator<int> e1 = c1.GetEnumerator();
        IEnumerator<int> e2 = c2.GetEnumerator();
        // var e1 = c1.GetEnumerator();
    }
}

 

위와 같이 IEnumerator객체를 만들면 된다.
이게 객체를 만들었으니 초기화를 하고, 요소를 꺼내볼 수 있다.
아래에서 초기화하고 요소를 꺼내오는 코드를 보자.

 

using System;
using System.Collections.Generic;

class Program {
    static void Main() {
        int[] arr = {1, 2, 3, 4, 5};
        
        List<int>       c1 = new List<int>(arr);
        LinkedList<int> c2 = new LinkedList<int>(arr);
        
        IEnumerator<int> e1 = c1.GetEnumerator();
        IEnumerator<int> e2 = c2.GetEnumerator();
        // var e1 = c1.GetEnumerator();
        
        e1.MoveNext(); // 최초 호출 - 초기화
        Console.WriteLine(e1.Current); // 첫 번째 요소 꺼내오기 - 1
        
        e1.MoveNext(); // 다음으로 이동
        Console.WriteLine(e1.Current); // 두 번째 요소 꺼내오기 - 2
    }
}

 

위와 같이 MoveNext()로 다음 요소로 이동 한 후 Current로 요소를 꺼내올 수 있다.
5개의 요소를 꺼내려면 5번을 해야하는데 딱 봐도 비효율 적이다.
그렇다면 모든 요소를 꺼내려면 어떤 방법이 있을까?

 

using System;
using System.Collections.Generic;

class Program {
    static void Main() {
        int[] arr = {1, 2, 3, 4, 5};
        
        List<int>       c1 = new List<int>(arr);
        LinkedList<int> c2 = new LinkedList<int>(arr);
        
        IEnumerator<int> e1 = c1.GetEnumerator();
        IEnumerator<int> e2 = c2.GetEnumerator();
        // var e1 = c1.GetEnumerator();
        
        while(e1.MoveNext()) // 더 이상 Next못 할 시 false 반환
            Console.WriteLine(e1.Current);
            
        while(e2.MoveNext())
            Console.WriteLine(e2.Current);
            
        e1.Reset();
        
        While(e1.MoveNext())
            Console.WriteLine(e1.Current);
    }
}

 

위와 같이 메모리가 연속되어 있는 List도, 떨어져있는 LinkedList도 같은 방법으로 사용 가능하다.
다시 처음 위치로 보내는 초기화는 Reset()이 있고 위와 같이 또 쓸 수 있다.

 ① 모든 컬렉션은 IEnumerable<T> 인터페이스를 구현한다.

    => 열거자를 꺼내는 GetEnumerator() 라는 메소드를 제공한다.

 

 ② 열거자

    => 컬렉션의 요소를 가리키는 객체

    => MoveNext(), Current, Reset() 멤버로 모든 요소에 접근 가능

    => 모든 열거자는 사용법이 동일하다.

    => 모든 열거자는 IEnumerator<T> 인터페이스를 구현하고 있다.

 


3. foreach 원리

using System;
using System.Collections.Generic;

class Program {
    static void Main() {
        int[] arr = {1, 2, 3, 4, 5};
        
        List<int> c1 = new List<int>(arr);
        
        foreach(int n in c1) {
            Console.WriteLine(n);
        }
    }
}

 

모두 알다시피 foreach문이 있어 모든 컬렉션의 요소를 편하게 뽑아볼 수 있다.
그런데 왜 위와같이 IEnumerator로 받아서 열거하는 방법을 설명했을까?
사실 foreach문을 사용하면 내부적으로 열거자를 사용하는 코드로 바뀌게 된다.
컴파일러가 바꾸는 코드는 아래와 같다.
열거자 개념을 통해 foreach가 동작하는 원리를 알아두면 되겠다.

 

using System;
using System.Collections.Generic;

class Program {
    static void Main() {
        int[] arr = {1, 2, 3, 4, 5};
        
        List<int> c1 = new List<int>(arr);
        
        foreach(int n in c1) {
            Console.WriteLine(n);
        }
        
        for(IEnumerator<int> p = c1.GetEnumerator(); p.MoveNext(); ) {
            int n = p.Current;
            Console.WriteLine(n);
        }
    }
}

 ① 모든 컬렉션은 foreach를 사용해서 열거 할 수 있다.

728x90
반응형