Memento's Programming

[C#] Foreach와 IEnumerator 본문

프로그래밍/C#

[C#] Foreach와 IEnumerator

대추언니 2019. 4. 15. 23:41

[1] foreach의 기본 문법

foreach문은 배열 또는 컬렉션(ArrayList, Stack, Queue, Hashtable 등)들을 순회하면서 각 데이터 요소에 차례대로 접근할 수 있도록 해줍니다. 기본 문법은 다음과 같습니다.

1
2
3
4
foreach(데이터형식 변수명 in 배열 or 컬렉션)
{
    // 코드
}

 

배열을 순회하는 foreach문을 예로 들어볼까요?

1
2
3
4
5
int[] arr = {1234};
foreach(int element in arr)
{
    Console.WriteLine($"{element}" ); // 1 2 3 4
}

 

이렇게 foreach문은 for문보다 훨씬 편리하게 쓸 수 있습니다. 하지만 foreach는 아무 형식의 객체에서나 사용할 수 있지 않습니다. 배열이나 리스트같은 컬렉션에서만 사용할 수 있죠. 그렇다면 이들은 어떻게 foreach문을 쓸 수 있는 것일까요?!

 

간단하게 말하면 모든 컬렉션들은 IEnumerable을 구현하고 있기 때문에 foreach문을 사용할 수 있습니다.

 

 

[2] IEnumerable과 IEnumerator

좀 더 자세히 들어가보겠습니다. foreach문을 위해 필수적으로 우리가 구현해야 하는 부분은 바로 IEnumerator 인터페이스입니다. 왜냐하면 IEnumerator 인터페이스는 foreach 구문에 필요한 멤버들로 구성 되어 있기 때문이죠. 따라서 IEnumerable 인터페이스에는 IEnumerator 형식의 객체를 반환하는 메서드(GetEnumerator)로 구성되어 있기 때문에 foreach문을 사용할 수 있는 것입니다.

 

1
2
3
4
5
6
7
8
9
10
11
interface IEnumerable
{
    IEnumerator GetEnumerator();
}
 
interface IEnumerator
{
    bool MoveNext();
    void Reset();
    Object Current { get; }
}

▶ MoveNext() : 다음 요소로 이동. 컬렉션의 끝을 지난 경우에는 false, 이동이 성공한 경우에는 false.

 Reset() : 컬렉션의 첫 번째 위치의 "앞"으로 이동. 첫번째 위치로의 이동은 MoveNext()를 호출한 이후에 이루어짐.

                   ex) 첫 번째 위치가 0이라면, Reset()을 호출한 이후로는 -1로 이동함.

 Current : 컬렉션의 현재 요소 반환

 

[3] Yield

그렇다면 IEnumerable에 있는 IEnumerator 형식의 객체를 반환하는 GetEnumerator 메서드에 대해서 좀 더 살펴보도록 하겠습니다. 우리는 GetEnumerator 메서드에서 yield 키워드를 사용하여 값을 하나씩 넘겨줍니다.

 

C#의 yield 키워드는 호출자에게 컬렉션 데이터를 하나씩 리턴할 때 사용합니다. 마치 일시정지의 기능이라고 생각하면 되는데요, yield는 yield returnyield break 로 사용할 수 있습니다.

 

 yield return : 현재 메소드의 실행을 일시 정지 시켜놓고 컬렉션 데이터를 하나씩 리턴하는 데에 사용. 메소드가 다시 실행 되면 yield return 혹은 yield break를 만날 때까지 나머지 작업을 실행함.

 yield break : 리턴을 중지하여 해당 메소드를 종료시킴.

 

 

만약 yield 키워드를 사용하지 않고 메소드에서 return을 했다면, 제어권이 호출자에게로 넘어가겠죠. 예를 들어 우리가 Main메소드에서 Console.WriteLine() 이라는 메소드를 호출했다면 Console.WriteLine() 메소드 내의 작업이 끝나는 순간 프로그램의 제어권은 호출자인 Main메소드로 넘어갈 것입니다.

 

하지만 yield return을 한다면 제어권이 Main에 완전히 넘어가지 않습니다. 단지 계속해서 값을 하나씩 리턴해 줄 뿐이지요. 이 yield와 Enumerator은 나중에 유니티의 코루틴을 이해하는 데에 핵심적인 부분이니 반드시 잘 짚고 넘어가야 합니다.

 


* 예제 *

이번에는 우리가 만든 클래스에 foreach를 사용할 수 있도록 코드를 작성해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;
using System.Collections;
 
namespace UsingForeach
{
    class MyEnumerator
    {
        int[] numbers = {1234};
        public IEnumerator GetEnumerator()
        {
            for (int i = 0; i < numbers.Length; i++)
            {
                yield return (numbers[i]);
            }
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            var obj = new MyEnumerator();
            foreach(int i in obj)
            {
                Console.Write($"{i} "); // 1 2 3 4
            }
        }
    }
}

 

Program클래스의 Main메소드에서 우리는 MyEnumerator 인스턴스를 생성하였습니다. 그리고 foreach문을 사용하여 MyEnumerator 내의 numbers 배열의 값을 하나씩 가져오고 있네요.

 

 

그렇다면 하나 의문점이 생깁니다. 우리는 MyEnumerator에서 IEnumerable 혹은 IEnumerator 인터페이스를 상속받지도 않았는데 어떻게 foreach문을 사용할 수 있는걸까요?

 

 

바로 Enumerator 타입을 반환하는 메소드 내에서 yield return을 하면 컴파일 타임에서 IEnumerator가 만들어지기 때문에 우리는 foreach문을 사용할 수 있는 것 입니다. 물론 MyEnumerator : IEnumerable, IEnumerator 처럼 MyEnumerator클래스가 IEnumerableIEnumerator 인터페이스를 상속받아 구현해도 우리는 foreach문을 사용할 수 있겠지요.

 

 

[4] IEnumerable과 IEnumerator를 쓰는 이유

마지막으로 그럼 우리는 왜 IEnumerableIEnumerator을 쓸까요? 너무 복잡하고 어려운데 말이죠. 이들은 순회하는 로직이 복잡한 경우 유용하게 쓸 수 있습니다. 예를 들어, 우리는 배열의 인덱스가 짝수인 요소들을 순회해야합니다. 그렇다면 GetEnumerator내의 for문을 이런 식으로 변경할 수 있겠죠. 이렇게 하면 우리는 foreach문을 사용해서 배열의 짝수의 요소들만 순회할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
using System.Collections;
 
namespace UsingForeach
{
    class MyEnumerator
    {
        int[] numbers = {1234};
        public IEnumerator GetEnumerator()
        {
            for (int i = 0; i < numbers.Length; i++)
            {
                if (i % 2 == 0) { yield return (numbers[i]); }
            }
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            var obj = new MyEnumerator();
            foreach(int i in obj)
            {
                Console.Write($"{i} "); // 1 3
            }
        }
    }
}
 
 

 

물론 우리는 Main메소드에서 다음과 같이 코드를 작성하면 위와 동일한 결과를 얻을 것입니다.

1
2
3
4
5
var obj = new MyEnumerator();
for (int i = 0; i < numbers.Length; i++// numbers가 public이라고 가정합니다.
{
    if (i % 2 == 0) { Console.Write($"{i} "); } // 1 3
}

 

하지만 우리가 매번 동일한 로직으로 순회한다고 생각해보세요. Enumerator를 구현해놓기만 하고 foreach문을 사용하기만 하면 우리가 원하는 결과가 나오는 데 훨씬 간단하지 않나요?! 이처럼 매번 복잡한 로직을 사용하여 순회하는 경우 이들을 묶어 GetEnumerator 내에 정의해주기만 하면 우리는 손쉽게 순회할 수 있을 것입니다.

 

 

퍼즐 게임이나 복잡한 알고리즘의 경우를 생각해보세요! 프로그램 내에 여기저기 흩어져 있었던 "짝수의 퍼즐"을 순회하는 로직을 우리는 깔끔하게 정리할 수 있을 것입니다.

 

 


[참고] 예제 코드 - 이것이 c#이다