[Unity] 열거자 인터페이스 IEnumerator

세동네·2022년 5월 13일
0
post-thumbnail

유니티 개발을 하면서 코루틴을 들어본 적 있을 것이다. 코루틴 함수를 선언할 때 반환형을 IEnumerator로 지정하는데, 이 인터페이스를 열거자라고 부른다. 열거자 IEnumerator에 대해 알아보자.

이번 포스트에서 다룰 인터페이스 IEnumeratorIEnumerable은 MS의 공식 문서에서 예제와 함께한 자세한 내용을 확인할 수 있다.

· 열거자 IEnumerator

유니티에서 IEnumerator는 작업을 분할하여 수행하는 함수라고 생각하면 편하다. 아래 예시를 보면 이해가 쉽다.

    IEnumerator GetNumberIEnumerator()
    {
        Debug.Log("IEnumerator : " + 1);
        yield return 1;
        Debug.Log("IEnumerator : " + 2);
        yield return 2;
        Debug.Log("IEnumerator : " + 3);
        yield return 3;
    }

열거자는 위와 같이 선언하며, 한 개 이상의 yield 반환문을 포함해야 한다. yield로 반환하는 것은 '일시적으로 CPU 권한을 다른 함수에 위임한다'라는 뜻이다.

'위임한다'는 말이 중요한데, 일반적인 함수는 반환하는 즉시 함수를 완전히 끝내는 것인데, 열거자는 권한을 잠시 위임하는 것이기 때문에 다른 함수로 권한을 넘기더라도 자신이 실행하고 있던 상태를 기억하고 있다.

일반적인 함수라면 아무리 호출하더라도 return 이후의 코드는 실행될 수가 없지만, 열거자는 호출할 때마다 이전에 권한을 위임한 시점부터 다시 코드를 실행한다.

열거자를 호출하는 코드는 아래와 같이 작성한다.

    void PrintNumber()
    {
        IEnumerator getNumberIEnumerator = GetNumberIEnumerator();
        getNumberIEnumerator.MoveNext();
        Debug.Log(getNumberIEnumerator.Current);
        getNumberIEnumerator.MoveNext();
        Debug.Log(getNumberIEnumerator.Current);
        getNumberIEnumerator.MoveNext();
        Debug.Log(getNumberIEnumerator.Current);
    }
/******************** 출력 ********************/

IEnumerator : 1
1
IEnumerator : 2
2
IEnumerator : 3
3

우선 열거자 객체를 생성하여 선언해 놓은 열거자 GetNumberIEnumerator를 저장한다. 그리고 열거자에 내장된 MoveNext() 함수를 호출하면 열거자에 CPU 권한을 위임하고, 다시 열거자가 권한을 넘길 때까지 코드를 실행한다.

GetNumberIEnumerator 열거자는 첫 권한을 부여받았을 때

Debug.Log("IEnumerator : " + 1);
yield return 1;

라인을 실행하고 다시 권한을 위임할 것이다. 이는 getNumberIEnumerator.Current을 이용해 열거자가 위임할 때 반환한 값이 1이라는 것을 확인할 수 있다. 이후 MoveNext() 함수를 호출하면 열거자가 마지막으로 실행되었던 Current부터 코드를 실행한다. 즉, 1을 반환했던 다음 라인인

Debug.Log("IEnumerator : " + 2);
yield return 2;

에 해당하는 코드를 실행한다.

MoveNext()으로 마지막 return까지 도달하여MoveNext()를 호출하여도 Current는 시작점으로 이동하지 않는다.


    void PrintNumber()
    {
        IEnumerator getNumberIEnumerator = GetNumberIEnumerator();
        getNumberIEnumerator.MoveNext();
        Debug.Log(getNumberIEnumerator.Current);
        getNumberIEnumerator.MoveNext();
        Debug.Log(getNumberIEnumerator.Current);
        getNumberIEnumerator.MoveNext();
        Debug.Log(getNumberIEnumerator.Current);
        getNumberIEnumerator.MoveNext();
        Debug.Log(getNumberIEnumerator.Current);
        getNumberIEnumerator.MoveNext();
        Debug.Log(getNumberIEnumerator.Current);
        getNumberIEnumerator.MoveNext();
        Debug.Log(getNumberIEnumerator.Current);
    }

위와 같이 코드를 작성한 결과는 아래와 같다.

Current를 시작점으로 옮기고 싶다면 getNumberIEnumerator.Reset() 함수를 호출하면 된다.

· 커스텀 객체의 열거 IEnumerable

또다른 열거자 활용법은 IEnumerable 인터페이스로 커스텀 클래스를 열거할 수 있게 만드는 것이다. IEnumerator는 열거자 그 자체이고, IEnumerable은 특정 객체가 열거될 수 있게 만들어준다. 이 방법으로 foreach와 같은 반복문을 사용할 수 없는 객체를 사용할 수 있게 만들어주는 편리함을 제공한다.

예를 들어, 특정 플레이어들을 그룹으로 묶어 그룹명과 함께 관리하고 싶다고 하자. 이를 위해 각 플레이어의 정보를 저장하는 클래스 Player와 그룹으로 관리하는 클래스 Group을 만들었다.

public class Player
{
    private string name;

    public Player()
    {
        name = "NoNamed";
    }

    public Player(string _name)
    {
        name = _name;
    }

    public string GetName()
    {
        return name;
    }
};

public class Group
{
    private Player[] players;
    private string group_name;

    public Group()
    {
        players = new Player[0];
    }

    public Group(Player[] _players)
    {
        players = new Player[_players.Length];

        for (int index = 0; index < _players.Length; index++)
        {
            players[index] = _players[index];
        }
    }

    public Player[] GetPlayers()
    {
        return players;
    }

    public string GetGroupName()
    {
        return group_name;
    }
}

간단한 예제를 위한 코드이므로 편의상 접근제한자는 public으로 통일하였다.

이때 Group의 객체를 만들고 그 안의 플레이어 이름을 나열하고자 한다면 다음과 같이 코드를 작성해야 할 것이다.

public class IEnumeratorTest : MonoBehaviour
{
    Player[] players;

    Group playerGroup;
    // Start is called before the first frame update
    void Start()
    {
        players = new Player[3]
        {
            new Player("Song"),
            new Player("Kim"),
            new Player("Lee")
        };

        playerGroup = new Group(players);

        foreach (Player player in playerGroup.GetPlayers())
        {
            Debug.Log(player.GetName());
        }
    }
}

이렇게 이름을 출력하기 위해 여러 함수를 찾아가야 하는 번거로움이 생긴다. 이때 IEnumeratorIEnumerable을 활용하면 이를 간단하게 만들어준다.

먼저 열거를 가능하게 할 클래스에 IEnumerable 인터페이스를 상속하여 열거가 가능하게 만들어준다. 이때 IEnumerable 인터페이스는 반드시 클래스 내부에 GetEnumerator() 함수를 포함해야 한다.

이때 GetEnumerator() 함수의 반환형이 IEnumerator이므로, 해당 인터페이스를 상속하는 클래스가 필요하다. 열거자 클래스는 PlayerEnum이라는 이름으로 생성하고, IEnumerator 인터페이스를 상속해준다.

위에서 살펴보았던 Current, MoveNext(), Reset()을 포함하였을 때 인터페이스를 사용할 수 있다. 그 형식은 아래와 같다.

public class PlayerEnum : IEnumerator
{
    private Player[] players;
    private int position = -1;

    public PlayerEnum(Player[] _players)
    {
        players = _players;
    }

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

    public Player Current
    {
        get
        {
            return players[position];
        }
    }


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

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

열거자를 상속한 클래스를 만들었으니, IEnumerable 인터페이스를 상속한 본래 클래스의 GetIEnumerator() 함수를 완성할 수 있다.

public IEnumerator GetEnumerator()
{
	return new PlayerEnum(players);
}

위 작업으로 열거가 가능해진 클래스는 열거할 멤버 객체를 직접 찾아 호출할 필요 없이 다음과 같이 foreach 등의 반복문에서 접근할 수 있게 된다.

void Start()
{
    players = new Player[3]
    {
        new Player("Song"),
        new Player("Kim"),
        new Player("Lee")
    };

    playerGroup = new Group(players);

    foreach (Player player in playerGroup)
    {
        Debug.Log(player.GetName());
    }
}

이를 완성한 최종 코드 전문은 아래와 같다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player
{
    private string name;

    public Player()
    {
        name = "NoNamed";
    }

    public Player(string _name)
    {
        name = _name;
    }

    public string GetName()
    {
        return name;
    }
};

public class Group : IEnumerable
{
    private Player[] players;
    private string group_name;

    public Group()
    {
        players = new Player[0];
    }

    public Group(Player[] _players)
    {
        players = new Player[_players.Length];

        for (int index = 0; index < _players.Length; index++)
        {
            players[index] = _players[index];
        }
    }

    public Player[] GetPlayers()
    {
        return players;
    }

    public string GetGroupName()
    {
        return group_name;
    }

    public IEnumerator GetEnumerator()
    {
        return new PlayerEnum(players);
    }
}

public class PlayerEnum : IEnumerator
{
    private Player[] players;
    private int position = -1;

    public PlayerEnum(Player[] _players)
    {
        players = _players;
    }

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

    public Player Current
    {
        get
        {
            return players[position];
        }
    }


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

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

public class IEnumeratorTest : MonoBehaviour
{
    Player[] players;
    Group playerGroup;
    
    void Start()
    {
        players = new Player[3]
        {
            new Player("Song"),
            new Player("Kim"),
            new Player("Lee")
        };

        playerGroup = new Group(players);

        foreach (Player player in playerGroup)
        {
            Debug.Log(player.GetName());
        }
    }
}

지금 당장은 사소한 편의성을 위해 귀찮은 작업을 하는 것처럼 보이지만, 클래스 구조가 복잡해지면 분명 굉장히 유용하게 사용할 수 있을 것 같다.

0개의 댓글