상세 컨텐츠

본문 제목

[C#] 열거자 (Enumerator) / Collection과 인덱서(indexer)

Study/Language

by Arq.Dev5igner 2021. 11. 15. 15:04

본문

List는 연속된 메모리에 요소가 놓이게 되기 때문에 , 메모리 공간을 떨어뜨려 놓을 수 없다.

이럴 때 흔하게 LinkedList를 사용하여 요소별로 메모리를 분리시킬 수 있다.
하지만 인덱서를 제공하지 않아 출력을 못한다는 문제가 있다.

인덱서를 제공하고 List -> LinkedList만 바꾸면 된다면, 바꿔가면서 성능을 비교해보면 좋을텐데 안된다는게 문제이다.

 

 

 

| 문제점.

Collection과 인덱서(indexer)

    => IList<T> 인터페이스를 구현한 컬렉션은 인덱서를 제공하지만, 메모리공간의 분리 불가능.

    => LinkedList<T>는  메모리공간의 분리가 가능하지만, 인덱서를 제공하지 않아 출력을 못함.

//List
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]);
         }
    }
}
 
//-----------------------------------------------------//

 //LinkedList
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
         }
    }
}

 

 

| 해결방안.

반복자(iterator) 패턴

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

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

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

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객체 생성
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();
    }
}

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

여기서 인터페이스 이름을 헷갈릴 수 있는데 

IEnumerable<T>은 컬렉션의 인터페이스이고
IEnumerator<T>는 열거자의 인터페이스이다.
따라서 반환값을 받을 때 IEnumerator<T>로 받으면 된다.
요즘은 var를 많이 사용한다. (여기서는 IEnumerator<T>를 사용한다.)

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

// 모든 요소를 꺼내오기 위한 효율적 방법.

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);
    }
}

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

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

 

 ② 열거자

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

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

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

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

 

 


| 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);
        }
    }
}



//==============================================================//

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문이 있어 모든 컬렉션의 요소를 편하게 뽑아볼 수 있다.
그런데 왜 위와같이 IEnumerator로 받아서 열거하는 방법을 설명했을까?
사실 foreach문을 사용하면 내부적으로 열거자를 사용하는 코드로 바뀌게 된다. 따라서 역for문을 사용하게된다. 
열거자 개념을 통해 foreach가 동작하는 원리를 알아두면 되겠다.

 

 

 

 

출처

[C#] 열거자 (Enumerator) (tistory.com)

'Study > Language' 카테고리의 다른 글

[C++ STL] list 개요  (0) 2022.02.25
[C++ STL] deque  (0) 2022.02.25
[C++ STL] map  (0) 2022.02.25
[C++ STL] std::vector 주요 멤버 함수 목록  (0) 2022.02.25
[C#] Collection과 Interface  (0) 2021.11.15

관련글 더보기