당연히 안됩니다. ContinuousDamage()라는 하나의 함수는 하나의 프레임 안에서 실행되기 때문에 우리가 눈으로 볼 수 있는 점차적인 감소는 보이지 않고, 그저 눈깜짝할 사이에 HP가 30이 빠져있는 것을 볼 수 있을 뿐입니다.
어떤 작업을 여러 프레임에 걸쳐서 늘려서 실행할 수 있게 고안된 함수가 바로 코루틴(Coruitne) 함수입니다.
코루틴 함수로 위의 예제의 함수를 만들면 이렇게 할 수 있습니다.
private void Start()
{
StartCoroutine(ContinuousDamage());
}
IEnumerator ContinuousDamage()
{
int time = 0;
while(time <3)
{
time++;
hp -= 10;
yield return new WaitForSeconds(1f);
}
}
일단 핵심적인 구현 함수는 IEnumerator 타입의 함수로 만들어야 합니다. 그리고 그 함수를 StartCoroutine()로 호출 할 수 있습니다.
그러면 1초마다 한번씩 10초동안 hp에서 -10을 연산하는 함수를 구현했습니다.
코루틴의 사용방식은 다양할 수 있지만 저는 기본적인 부분만 언급하고 넘어가도록 하겠습니다. 이 포스팅의 의도는 코루틴을 사용하는 법을 익히는데 있지 않고 코루틴을 통해 이전의 포스팅에서 살펴본 개념들을 적용할 수 있게 되는 것이기 때문입니다.
이 부분에서 우리가 함께 자세히 들여다 보고 싶은 것은 IEnumerator와 yield return 그리고 코루틴의 작동 방식입니다.
지난 포스팅에서 우리는 IEnumerator와yield return가 어떻게 작동하는지 그 원리를 함께 살펴봤습니다.
IEnumerator는 어떤 인덱스적인 요소가 있는 대상 혹은 객체가 있다면 그 인덱스적 요소에 순차적으로 접근하고 그 접근한 요소를 반환해주는 기능을 하는 인터페이스입니다.
그리고 yield return은 함수 안에 존재하는 내용을 인덱스 적인 부분으로 나눠주는 역할을 하는 키워입니다.
그런 의미를 생각하면서 위의 예제 부분을 다시 살펴보겠습니다.
while이 사용되어서 그렇지 사실 for문을 사용해도 무방하고 3초밖에 안되니 풀어써도 무방합니다. 이해를 돕기 위해서 반복문의 사용없이 함수를 풀어 써보겠습니다.
IEnumerator ContinuousDamage()
{
hp -= 10; // 1번 부분
yield return new WaitForSeconds(1f);
hp -= 10; // 2번 부분
yield return new WaitForSeconds(1f);
hp -= 10; // 3번부분
yield return new WaitForSeconds(1f);
}
IEnumerator 타입의 함수에 yield return이 사용되는 순간 함수는 기준으로 3개의 부분으로 나뉘게 됩니다.
요소가 3게인 인덱스적인 대상으로 바뀌었다고 표현할 수 있게 됩니다.
이 함수의 반환타입인 IEnumerator의 변수에 넣는다면 IEnumerator의 기능에 구현된 Move.Next()와 Current를 이용해서 이 함수의 각 요소에 순차적으로 접근할 수 있게 됩니다.
이 경우 순차적인 요소란 함수의 각 부분에 실행되어야 하는 코드들입니다.
IEnumerator의 기능으로 1번부분의 코드와 2번 부분의 코드, 3번 부분의 코드를 반환 받을 수 있다는 겁니다.
자 그럼 이 IEnumerator함수를 호출하는 Startcorutine() 함수는 어떤 의미의 함수일까요?
제가 편의상 IEnumerator함수를 호출하는 함수라고 했지만 사실 형식으로 보면 IEnumerator함수를 호출하는게 아니라 매개변수로 사용하는 겁니다.
그래서 StartCoroutine(ContinuousDamage()); 이런 형식으로 사용하게 되는 겁니다.
StartCoroutine 함수는 유니티에서 만든 비공개 함수임으로 그 내용을 정확하게 알 수는 없지만 대략 IEnumerator와 yield return을 이해한다면 어느정도 유추가 가능합니다.
아마 StartCoroutine 함수는 이런 형식이 아닐까 싶습니다.
void StartCoroutine(IEnumerator enumerator)
{
while (enumerator.MoveNext())
{
WaitTime(enumerator.Currnet)
}
}
void WaitTime(object waittime)
{
// waitForSeconds 객체에서 받아온 float 값의 시간 만큼
// 메인 스레드의 다른 코드를 먼저 실행시키는 함수
}
당연히 유니티에서 제공하는 StartCorutine 함수는 이런 로직보다 훨씬 복잡하겠지만 작동원리를 이해하기 위해서 아주 간략하게 만들어봤습니다.
우리가 유니티 스크립트에서 StartCorutine() 함수를 호출하면 아마도 위에 있는 예제와 비슷하게 생긴 함수를 호출하는 것입니다. 그리고 StartCorutine는 IEnumerator타입의 객체를 매개변수로 받기 때문에 우리가 코루틴을 사용할때 만드는 IEnumerator 함수의 반환 값이 매개 변수에 할당해서 StartCorutine( IEnumerator 반환값) 이 호출되게 되는 겁니다.
그럼 StartCorutine( IEnumerator 반환값)이 호출될때 어떤 일이 일어나는지 한번 자세히 살펴보겠습니다.
void StartCoroutine(IEnumerator enumerator)
{
while (enumerator.MoveNext())
{
WaitTime(enumerator.Currnet)
}
}
StartCorutine 함수에서 매개변수 enumerator에 IEnumerator 타입의 객체를 받으면 이제 enumerator.MoveNext()가 true를 반환할 동안은 반복문을 실행하게 됩니다.
enumerator.MoveNext()가 true를 반환한다는 것은 어떤 의미일까요?
enumerator.MoveNext()는 bool값을 반환하는데 IEnumerator에 할당된 객체 혹은 대상에게 현재요소에서 다음으로 넘어갈 요소가 있다면 true를 만일 요소가 없다면 false를 반환합니다. 다시 말해서 마지막요소까지 가기 전까지 true를 반환하고 다음요소로 가지 못한다면 false를 반환합니다.
그러니 enumerator.MoveNext() true를 반환한다면 다시 말해서 IEnumerator 객체에 다음요소가 존재한다면 while문 안에 있는 로직을 실행하라는 의미입니다.
그럼 while문 안에는 어떤 내용이 있을까요?
WaitTime(enumerator.Currnet)
WaitTime은 제가 임의로 만들어낸 함수입니다. 내용은 아마 너무 복잡한 내용일것 같아서 간단히 주석으로 대략적인 기능만 설명해 놓았습니다.
void WaitTime(object waittime)
{
// waitForSeconds 객체에서 받아온 float 값의 시간 만큼
// 메인 스레드의 다른 코드를 먼저 실행시키는 함수
}
여기서 중요한 것은 매개변수입니다.
WaitTime(enumerator.Currnet) 을 본다면 이 함수가 매개변수로 전달하고 있는 것은 enumerator 객체가 가지고 있는 현재 반환 값입니다.
그런데 코루틴에서는 어떤 값이 반환될까요?
yield return new WaitForSeconds(1f);
네, yield return 뒤에 오는 new WaitForSeconds(1f) 값이 반환됩니다.
따라서 TimeAttack (enumerator.Currnet)은 TimeAttack(new WaitForSeconds(1f)) 이 됩니다.
그럼 new WaitForSeconds(1f)이 의미하는 바는 뭘까요?
WaitForSeconds는 사실 유니티에서 만든 클래스 이름입니다.
그러니까 WaitForSeconds() 라고 하면 WaitForSeconds 클래스의 생성자가 되는 겁니다.
WaitForSeconds생성자는 매개변수로 float값을 받습니다. 그것을 시간이라고 치고 계산을 하겠다는 겁니다.
그럼 왜 앞에 new가 오는지 이해가시죠?
바로 WaitForSeconds클래스의 새로운 객체를 생성하는 겁니다.
그러니까 일반적으로 코루틴을 사용할때 new WaitForSeconds()를 사용하면 계속해서 새로운 객체가 생성되는 겁니다. 만일 이 객체를 파괴하지 않고 새로운 코루틴을 계속해서 호출한다면 사용하지 않는 객체가 계속 쌓여서 성능에 악영향을 끼치게 되는 겁니다.
그래서 보통 코루틴을 사용할때 미리 전역변수에 new WaitForSeconds() 을 할당해서 사용하라고 하는 겁니다.
아무튼 이 WaitForSeconds 클래스가 어떤 기능을 가지고 있는지 정확히는 알지 못하지만 우리는 코루틴을 사용하는 법을 알고 있으므로 어느정도 유추가 가능합니다.
아마 매개 변수로 받는 float값을 시간으로 생각하고 그만큼의 시간을 계산해주는 역할을 하지 않을까 싶습니다.
마치 우리가 Update() 함수에서 Time.deltaTime을 통해 시간 계산 식을 만드는 것 처럼 말입니다.
그리고 이제 임의로 만든 TimeAttack함수의 매개변수로 사용해 WaitForSeconds 이 계산한 시간 동안에 동일한 스레드의 다른 코드들을 실행하는 로직이 구현되어 있을 겁니다.
아마 우리가 잘 알지는 못해도 유니티는 내부적으로 무슨 코드를 먼저 실행하지에 대한 일련의 원칙들을 정해놨을 것 같습니다. 그리고 WaitForSeconds이 반환해주는 시간 값 만큼 후순위의 다른 코드들을 먼저 실행시키는 식으로 아마 내부적인 처리를 했을 것으로 예상할 수 있습니다.
물론 이건 어디까지나 제 예상입니다. 실제로 어떤 식으로 구성되어있는지는 공개되어 있지 않고 코루틴이 하나만 동작되는 것도 아니기때문에 훨씬 더 여러개의 IEnumerator 객체를 다루는 시스템이 구현되어 있을 것입니다.
예제로 다시 돌아오겠습니다.
void StartCoroutine(IEnumerator enumerator)
{
while (enumerator.MoveNext())
{
TimemAttack(enumerator.Currnet)
}
}
위의 코드를 보시면 enumerator의 다음 요소가 없을때까지 실행하는 것은 enumerator.Currnet으로 인해 받은 시간 만큼 코드 실행을 유니티 엔진이 가지고 있는 우선순위를 가지고 있는 다른 코드들을 먼저 실행시키는 과정을 거치는 겁니다.
그리고 그 시간이 지나면 다시 enumerator.MoveNext()를 실행해서 yield return new WaitForSeconds(1f) 의 다음 코드들을 실행하고 다시 유니티 엔진에게 다른 정해진 시간 만큼 다른 코드들을 먼저 실행시켜달라고 요청하는 겁니다.
이런 식으로 더 이상 반환 받을 yield return new WaitForSeconds(1f) 가 없을때 까지 작동하게 되는 겁니다.
굳이 좀 더 예상해보자면 StopCoroutine()함수는 아마 코루틴 함수로 받은 IEnumerator 객체를 관리하는 리스트에서 해당 IEnumerator 객체를 제거하는 기능을 하지 않을까 싶습니다.
아무튼 여기까지 이해하셨다면 대략적으로 코루틴과 IEnumerator 그리고 yield return에 대해서 이해하신 겁니다.
유니티에서 제공하고 있는 코루틴 사용시 사용할 수 있는 클래스들이 있습니다.
WaitForSeconds: 지정된 시간(초) 동안 대기합니다.
WaitForFixedUpdate: 고정된 물리 프레임마다 대기합니다.
WaitForEndOfFrame: 현재 프레임의 렌더링이 완료될 때까지 대기합니다.
WaitUntil: 조건이 참이 될 때까지 대기합니다.
WaitWhile: 조건이 거짓인 동안 대기합니다.
이런 클래스들은 모두 유니티에서 코루틴을 사용할 시 yield return 뒤에 붙여 사용하도록 만든 클래스들이고 이들은 클래스들이기 떄문에 이들을 사용할때 최소 한번은 객체를 생성하는 New 키워드를 사용해야하는 겁니다.
지금까지 꽤 긴 분량으로 유니티의 코루틴에 대해서 함께 살펴봤습니다.
계속 언급했지만 이번 포스팅의 목적은 코루틴의 실행 방법을 익히는 것이 아니라 코루틴이라는 기능을 공부하면서 C#의 문법들을 좀 더 이해하고 응용할 수 있는 방식을 살펴보는 것이었습니다.
C#문법을 공부할때 따로 따로 공부했던 개념들을 코루틴이라는 하나의 주제로 모아서 연관지어서 이해하면 좀 더 실전적인 이해와 응용력을 기르는데 도움이 되지 않을까 싶어서 정리해봤습니다.
지난 두 번의 포스팅으로 인터페이스(Interface)와 IEnumerator 그리고 IEnumerable에 대해 살펴보았습니다.
코루틴을 이해하는데 인터페이스 부터 시작하는건 좀 너무한것 아니냐는 분도 계실지 모르겠지만, 단순히 코루틴의 사용법을 익힌다기보다 코루틴까지 가는 과정을 이해하면서 C#문법과 유니티 함수에 대해서 조금 더 깊이 이해하고 응용할 수 있게 되는 것이 이 글의 목표라고 생각해주시면 되겠습니다.
아무튼 이제 긴 시간을 거쳐 드디어 코루틴에 대해 말할 수 있게 되었냐고 한다면 아직은 아닙니다.
코루틴을 가기전에 'yield return'에 대해 이해하고 나면 좋을 것 같습니다.
yield return
yield return은 C#에서 제공하는 키워드로 주로 IEnumerator, IEnumerable 타입을 반환하는 함수에서 사용됩니다.
yield return은 함수 안에 블록을 지정하고 그 블록안에 있는 요소를 차례로 반환해주는 역할을 합니다.
예를 들어보겠습니다.
지역변수 int 타입의 배열 numbers = {1 ,2,3} 이 있습니다. 내가 원하는 것은 GetNumers라는 함수를 하나 만들어 GetNumbers를 호출할때 첫번째 호출에서는 numbers의 첫번째 요소를 두번째 호출 할때는 두번째 요소를 반환하는 함수를 만들고 싶습니다.
이런 식으로 해야할 텐데.. 만일 number의 요소가 100까지 있다면 저런식으로 100줄을 만들어 각 요소를 불러줘야 할겁니다. 게다가 다른 곳에서 GetNumbers의 매개변수를 0부터 각각 순서대로 집어넣어 준다면 방금 전까지 어느요소를 호출했는지 그 인덱스를 기억해야하는 일까지 생깁니다. 너무 피곤하겠죠?
이로써 알 수 있는 것은 yield return은 어디까지 반환했는지를 기억했다가 다음 호출할때에는 그 다음요소를 넘겨주는 역할을 하는 키워드 입니다.
yield return을 사용하는 함수의 동작원리를 좀 더 잘 이해하기 위해 위의 코드를 다음과 같이 바꿔봤습니다.
IEnumerator GetNumbers()
{
int num = 1; // 1번 연산 부분
yield return num; //1번 연산 부분에서 연산한 값을 리턴 한후 멈춤
num++; //2번 연산 부분
yield return num; // 2번 연산을 실행한 후 그 값을 리턴
num++; // 3번 연산부분
yield return num; // 3번 연산을 실행한 후 그 값을 리턴
}
이 함수는 바로 위에 있는 함수와 완전히 같은 기능을 하는 함수입니다. 차이는 yield return 사이에 연산하는 코드들이 있다는 거겠죠?
다시 말해 yield return은 함수 안에서 실행되는 구역을 나눠주는 역할을 하는겁니다. 그리고 함수가 호출 될 때마다 첫번째 구역 다음 호출될때 두번째 구역 이런식으로 실행해 그 값을 반환하는 겁니다.
그럼 yield return을 int 형 함수나 void 형 함수처럼 일반 적인 형태의 함수에서 사용할 수 있을까요?
아쉽지만 일반 함수에서는 사용할 수 없고 반환타입이 반드시 IEnumerable 혹은 IEnumerator인 함수에만 사용할 수 있습니다.
정확히는 IEnumerator타입 함수를 사용해야하는 것이지만 IEnumerable 타입은 그안에 IEnumerator를 불러오는 기능을 포함하고 있기 때문에 사용할 수 있게 되는 겁니다.
IEnumerator가 구현하는 기능은 무엇이었을까요?
IEnumerator 는 어떤 대상의 순차적으로 되어 있는각 요소에 접근하고(Next.Move()) , 접근한 그 순번째의 요소를 반환하고(object Currunt), 혹시 마지막 요소까지 모두 접근이 끝났지만 다시 처음부터 반복하고 싶을때 순번을 되돌리는 기능(Reset())을 가지고 있습니다.
따라서 함수 안에서 yield return을 사용하면 yield return을 기점으로 함수를 실행하는 요소가 순차적으로 생성된 것입니다. 위의 코드로 설명해보자면 yield return을 기준으로 1번 연산 부분, 2번 연산 부분, 3번 연산 부분으로 3개의 순차적인 요소가 생성 되는 겁니다.
그럼 이 함수안에 나눠진 구역을 요소로 생각한다면 IEnumerator 가 하는 일은 MoveNext()를 실행해서 그 1번 연산 부분에 접근하고, Currnt를 이용해서 1번연산의 결과값을 리턴하는 요소를 가져올 수 있습니다. 그리고 만일 요소가 다 끝났다면 Reset()를 실행하여 처음부터 다시 이과정을 실행할 수 있는 겁니다.
자 그럼 이제 위에서 구현한 IEnumerator 함수를 어떻게 사용하는지 살펴보겠습니다.
GetNumbers()함수를 호출 될때 마다 각각 1,2,3 이렇게 출력이 되게 만들고 싶습니다.
그런데 만일 처음 int 배열을 사용했던 스크립트 처럼 그냥 함수 자체를 출력하려고 한다면 원하는 값이 나오지는 않습니다.
먼저 GetNumbers(); 함수로 반환되는 IEnumerator의 객체를 생성해 IEnumerator 타입 변수 ienumerator 에 할당합니다.
(곧바로 Debug.Log( GetNumbers()); 이런 형식으로 사용하지 않는 이유는 GetNumbers(); 를 호출할 때 마다 IEnumerator의 객체가 새로 생성되기 때문입니다. 그렇다면 Debug.Log(GetNumbers()); 를 사용할 때마다 첫번째이전 요소만 출력하게 되지 순차적으로 다음 요소를 출력할 수 없게 됩니다. 그래서 먼저 GetNumbers() 명령을 통해 IEnumerator의 객체를 하나 생성해주고 그 객체를 사용함으로서 연속된 인덱스에 순차적으로 접근할 수 있게 됩니다.)
그리고 먼저 ienumerator의 첫번째 요소에 접근(MoveNext)하는 명령을 내려주고, 그 요소를 출력하라는 명령 (Current) 을 내려려서 출력해줍니다. 이런식으로 다음요소에 접근해 출력하고 또 다음 요소에 접근해 출력하면 우리가 원하는 결과인 1,2,3이 순차적으로 출력되게 되는 겁니다.
게다가 이렇게 순차적인 출력을 start함수에서 한번에 하는 것이 아니라 각각 다른 함수에서 다른 시점에서 내가 몇번째 요소까지 호출했는지 기억할 필요 없이
ienumerator.MoveNext();
Debug.Log(ienumerator.Current)
이 두 줄을 사용하면 각 요소를 순차적으로 출력할 수 있게 된다는 말입니다.
그렇다면 IEnumerable 과 yield return은 어떻게 함께 사용하는 것일까요?
이번에는 GetNumbers()를 IEnumerable 반환타입 함수로 만들었습니다. 함수 내용은 이전과 같습니다.
IEnumerable은 IEnumerator의 기능을 사용할 수 있는 환경 자체를 인식하고 기능을 전달하는 기능을 가지고 있습니다. 어떤 대상이 IEnumerator를 사용할 수 있도록 일정한 인덱스 요소를 가지고 있다면 IEnumerable 타입으로 변환이 가능하다는 의미입니다.
여기서는 IEnumerable GetNumbers() 함수 내부에서 yield return를 사용하는 순간 함수 내부의 1,2,3 각 연산부를 인덱스로 보고 그 인덱스 구조 자체를 IEnumerable 타입에 담아서 반환할 수 있다는 이야기입니다.
그리고 인덱스 구조가 있다면 IEnumerator의 기능을 사용할 수 있게 됩니다.
따라서 IEnumerable 타입의 객체가 반환되었다면 우리는 그 객체에 IEnumerator기능을 사용할 수 있게 되었다는 것을 의미하며 그 기능을 사용하기 위해서 다음과 같은 작업이 필요합니다.
IEnumerator ienum = GetNumbers().GetEnumerator();
Debug.Log( GetNumbers().GetEnumerator() ) 이렇게 사용해도 될 것 같지만 위의 그전 예제에서 IEnumerator 객체를 따로 할당해서 사용했던 것과 동일한 이유로 ienum 이라는 IEnumerator 타입 변수에 할당해서 인덱스의 값이 연속적으로 출력될 수 있도록 합니다.
그리고 Move.Next와 Current를 사용해서 요소에 접근하고 출력할 수 있게 되는 겁니다.
IEnumerable를 사용할때에는 IEnumerable안에 들어가는 객체의 요소에 반드시 인덱스 적인 요소가 필요합니다.
인덱스적라는 말을 해석하자면 순차적으로 접근할 수 있는 구분된 요소들이 존재해야한다는 겁니다.
(사실 이해를 돕기 위해 인덱스라는 말을 사용했지만 보통 '시퀀스'라는 말을 사용합니다. )
위의 예제에서는 yield return이 함수의 부분을 인덱스적으로 만들어주는 역할을 하는 겁니다. 다시말해 순차적인 요소를 구분하는 일종의 구분선의 역할을 해준다고 보면 되는 겁니다.
만일 yield return를 사용하지 않는 IEnumerable 타입 반환 타입의 함수를 만든다면 반환되는 값은 반드시 인덱스적인 요소를 가지고 있는 것이어야 합니다.
그렇기 때문에 IEnumerable안에 배열이나 컬렉션을 반환 요소로 삼아도 되는 겁니다.
그래서 IEnumerable 함수 안에 배열을 넣어 반환했다면 똑같이 IEnumerator기능을 불러와 사용할 수 있게 되는 겁니다.
자 오늘 이야기를 정리해 보겠습니다.
1. IEnumerator 는 어떤 인덱스 적인 요소에 순차적으로 접근해서 그 요소를 반환해주는 기능을 가지고 있습니다.
2. IEnumerable은 어떤 대상 혹은 객체가 인덱스적인 요소를 가지고 있는지를 인식해주는 역할을 합니다.
따라서 어떤 대상이나 객체가 IEnumerable 타입에 할당될 수 있다면 그 대상은 반드시 IEnumerator의 기능들을 사용할 수 있는 구조를 가지고 있다는 의미입니다.
3. yield return은 바로 함수 안에 인덱스적인 구조를 나눠주는 역할을 하는 키워드입니다. yield return 가 들어가는 함수는 '실행하는 요소가 순차적으로 구분되어 있는 구조'로 바뀌게 됩니다. 따라서 이러한 구조를 이해하고 사용할 수 있는 IEnumerable나 IEnumerable가 아니면 yield return가 있는 함수의 구조를 이해하거나 해석할 수 없게 되는 겁니다.
4. 그래서 yield return은 반드시 IEnumerable나IEnumerable가 반환되는 함수에만 사용할 수 있게 되는 겁니다.
IEnumerable가 하는 일은 객체에 포함된 열거적 요소를 사용할 수 있는 기능을 불러오는 기능입니다. 외부에서 사용할 수 있게 IEnumerator을 연결시켜주는 역할만 한다고 보시면 되겠습니다.
다시 말해 어떤 기능을 해서 결과물을 만들어내는 주체(IEnumerator)가 있고 그 결과물을 외부로 전달해서 외부에서 그것을 가공할 수 있게 해주는 매개체역할(IEnumerable)이 있는 겁니다.
그래서 이 둘은 거의 함께 합니다.
이것도 어려우니까 예를 들어보겠습니다.
int[] number = new int[] { 0, 1, 2, 3 };
// 1. IEnumerable가 array에 구현되어 있기 때문에 변수로 할당하는 것이 가능.
IEnumerable Inum = number;
//2. IEnumerable 안에는 GetEnumerator()가 존재하기 때문에
// array에 존재하는 IEnumerator를 가져다 ienumerator에 할당시킴.
IEnumerator ienumerator = Inum.GetEnumerator();
ienumerator.MoveNext(); //arrry안에 있는 첫번째 요소 접근
Debug.Log(ienumerator.Current); // 접근한 요소 반환해서 출력.
ienumerator.MoveNext();// arrry안에 있는 두번째 요소 접근
Debug.Log(ienumerator.Current); // 접근한 요소 반환해서 출력.
ienumerator.MoveNext();// arrry안에 있는 세번째 요소 접근
Debug.Log(ienumerator.Current);// 접근한 요소 반환해서 출력.
ienumerator.MoveNext();// arrry안에 있는 네번째 요소 접근
Debug.Log(ienumerator.Current);// 접근한 요소 반환해서 출력.
이 예제를 설명하기 전에 C#에서 제공하는 array(배열)에 대해 살짝 이해하고 넘어가야 합니다.
배열은 컬렉션의 일부로 C#에서 구현해놓은 키워드입니다. 정해진 길이의 데이터를 인덱스 요소로 관리 하기 위해서 만든 클래스입니다.
그런데 이런 array를 비롯한 모든 컬렉션과 컬렉션<T>의 클래스에는IEnumerable과IEnumerator이 구현되어 있습니다.(상속되어 있습니다.)
따라서 array나 다른 컬렉션을 사용할때에는 그 안에 반드시IEnumerable과IEnumerator의 요소들이 포함되어 있는 겁니다. 우리가 평소 사용할때 보이지는 않지만IEnumerable과IEnumerator의 요소들을 꺼내서 사용할 수 있으며 그것을 사용해서 구현한 것이 forech입니다.
따라서 forech를 사용할 때 입력하는 변수는 반드시 System.Collections 이나 System.Collections.Generic에 포함된 컬렉션 이어야 합니다.
이제 위의 예제를 살펴보겠습니다.
int[] number = new int[] { 0, 1, 2, 3 };
IEnumerable Inum = number;
먼저 int 0,1,2,3의 요소를 가진 배열 number를 선언했습니다.
그리고IEnumerable의 타입을 가진 Inum이라는 변수에 number 배열을 할당합니다.
이게 가능한 이유는 지난시간 설명했던 인터페이스 예제에서 Weapon 클래스의 객체인 sword 객체를 IDurability 데이터 타입의 변수에 할당할 수 있던것과 같은 이유입니다. array 클래스에는IEnumerable인터페이스가 구현되어 있기 때문에 가능합니다.
IEnumerable 타입의 변수에 담을 수 있다 혹은 IEnumerable 타입으로 반환할 수 있다는 것은 그 대상 혹은 객체가 순차적으로 접근할 수 있는 요소들을 반드시 포함한다는 의미입니다.
그렇기 때문에 IEnumerable의 타입의 변수를 통해 할 수 있는 혹은 하려는 일은 하나입니다. 순차적인 각 요소에 접근해 값을 받아오는 일입니다.
그리고 그일을 가능하게 하기 위해서 IEnumerable 인터페이스가 구현하는 기능은 딱 하나입니다.
순차적인 요소에 접근해 요소를 반환하는 기능을 가진 IEnumerator를 불러오는 기능 바로 GetEnumerator()입니다.
따라서IEnumerable의 데이터 타입을 가진 Inum이 사용할 수 있는 참조형태는 딱 하나입니다.
Inum.GetEnumerator() 이거죠. 그럼 Inum안에 있는IEnumerator의 기능을 실행시킬 수 있게 되는 겁니다.
그리고 그 실행시키는 기능 자체를IEnumerator 데이터 타입의 변수ienumerator 에 담습니다.
IEnumerator ienumerator = Inum.GetEnumerator();
그럼 이제ienumerator 의 프로퍼티를 사용하면IEnumerator의 세가지 기능을 사용할 수 있게 됩니다.
ienumerator.MoveNext()
ienumerator .Currunt;
ienumerator .Reset();
이렇게 말이죠.
ienumerator는 IEnumerator의 기능을 가지고 있는 클래스입니다. 따라서 위의 세가지 기능을 사용한다면ienumerator에 할당되어 있는 클래스 객체인 number에서 이 기능이 실행되는 겁니다.
ienumerator.MoveNext(); //arrry안에 있는 첫번째 요소 접근
Debug.Log(ienumerator.Current); // 접근한 요소 반환해서 출력.
ienumerator.MoveNext();// arrry안에 있는 두번째 요소 접근
Debug.Log(ienumerator.Current); // 접근한 요소 반환해서 출력.
ienumerator.MoveNext();// arrry안에 있는 세번째 요소 접근
Debug.Log(ienumerator.Current);// 접근한 요소 반환해서 출력.
ienumerator.MoveNext();// arrry안에 있는 네번째 요소 접근
Debug.Log(ienumerator.Current);// 접근한 요소 반환해서 출력.
사실 반복문이기 때문에 forech를 사용해도 되지만 어떤 식으로 동작하는지 알기 위해서 나열해 봤습니다.
배열 클래스 number가 할당 된ienumerator에서 MoveNext()를 실행하면 배열의 첫번째 요소에 접근합니다.
(MoveNext()를 사용하기 전에 접근해 있는 요소는 -1요소로 null이라고 나옵니다.)
그다음ienumerator.Current 실행을 통해 접근한 첫번째 요소를 출력합니다. 0이 출력됩니다.
그 다음은 설명하지 않아도 괜찮겠죠?
지금까지IEnumerator와 IEnumerable 인터페이스의 기능을 이해하고 이러한 기능을 C# 컬렉션에서 어떤 식으로 이용하는지 살펴봤습니다.
다음 시간에는 IEnumerator와 IEnumerable의 이해를 바탕으로 코루틴을 사용할때 함께 사용되는 yield return에 대해 이해해보는 시간을 갖도록 하겠습니다.