목록을 돌려주는 함수는 보통 목록을 다 만든 다음에 돌려줍니다. yield return 을 쓰면 함수가 값을 하나 내놓고 멈췄다가, 다음 값을 달라고 할 때 이어서 실행합니다. 이번 편에서는 foreach 가 그 뒤에서 무엇을 하는지, 끝없는 목록과 중간에 멈추는 목록이 어떻게 동작하는지 봅니다.
2024년 부산 콘텐츠 아카데미 퍼스널 트레이닝 수업에서 MyLesson Unity 실습 프로젝트의 Strategy_06_IEnumarable.cs 로 가르친 내용입니다. 수업은 다섯 단계로 나뉘고, 단계마다 코드가 조금씩 바뀝니다.
멈췄다 이어 가는 함수
private IEnumerable<string> IntRangeGroup(string first, string last, int min, int max)
{
yield return first;
for (int i = min; i <= max; i++)
{
yield return i.ToString();
}
yield return last;
}
public void Run()
{
foreach (string value in IntRangeGroup("시작", "종료", 0, 5))
{
Program.WriteLine(value);
}
}시작
0
1
2
3
4
5
종료IntRangeGroup 은 return 으로 목록을 한 번에 돌려주지 않습니다. yield return 을 만날 때마다 값을 하나 내놓고 그 자리에서 멈춥니다. foreach 가 다음 값을 달라고 하면, 멈췄던 곳 다음 줄부터 이어서 실행합니다. 함수 안의 지역 변수 i 도 그대로 기억합니다.
foreach 를 풀어 쓰면
public void Run()
{
IEnumerable<string> enumerable = IntRangeGroup("2부터", "9까지", 2, 9);
using (IEnumerator<string> enumerator = enumerable.GetEnumerator())
{
while (enumerator.MoveNext())
{
Program.WriteLine(enumerator.Current);
}
}
}foreach 는 이 코드를 줄여 쓴 것입니다. GetEnumerator() 로 "꺼내는 도구" 를 받고, MoveNext() 로 다음 값으로 넘어가며, Current 로 지금 값을 읽습니다. MoveNext 가 false 를 돌려주면 끝입니다.
| 이름 | 역할 |
|---|---|
IEnumerable<T> | 꺼낼 수 있는 목록. GetEnumerator() 하나만 약속 |
IEnumerator<T> | 꺼내는 도구. MoveNext(), Current, Dispose() |
MoveNext() | 멈춘 곳부터 다음 yield return 까지 실행하고, 값이 있으면 true |
using (…) | 다 쓰면 Dispose() 를 불러 정리. foreach 도 몰래 해 준다 |
1편에서 배열과 List 를 IEnumerable 로 받았던 이유가 여기서 이어집니다. yield 를 쓰는 함수도 IEnumerable 이라, 같은 foreach 로 돌 수 있습니다.
끝없는 목록
private IEnumerable<int> InfinityIncrement()
{
int current = 0;
while (true)
{
yield return current;
current++;
}
}
public void Run()
{
foreach (int value in InfinityIncrement())
{
Program.WriteLine(value);
if (value > 15)
{
break;
}
}while (true) 안에서 yield return 을 하는 함수는 끝나지 않습니다. 그래도 프로그램은 멈추지 않습니다. 값은 달라고 할 때 하나씩만 만들어지기 때문입니다(지연 실행). 받는 쪽이 15 를 넘었을 때 break 하면, 함수는 그 자리에 멈춘 채 버려집니다.
0
1
2
…
15
16yield break 와 finally
private IEnumerable<string> FinallyEffect(string first, string last, int min, int max, int endCondition)
{
try
{
Program.WriteLine(first);
for (int i = min; i <= max; i++)
{
Program.WriteLine("Start of loop");
yield return i.ToString();
if (i == endCondition)
{
yield break;
}
Program.WriteLine("End of loop");
}
Program.WriteLine(last);
}
finally
{
Program.WriteLine("Finally block");
}
}public void Run()
{
foreach (string value in FinallyEffect("시작", "끝", 0, 4, 5))
{
Program.WriteLine("value: {0}", value);
}
Program.WriteLine();
foreach (string value in FinallyEffect("시작", "끝", 0, 5, 2))
{
Program.WriteLine("value: {0}", value);
}
}시작
Start of loop
value: 0
End of loop
Start of loop
value: 1
End of loop
…
Start of loop
value: 4
End of loop
끝
Finally block
시작
Start of loop
value: 0
End of loop
Start of loop
value: 1
End of loop
Start of loop
value: 2
Finally block출력 순서를 보면 실행이 두 함수 사이를 오가는 것이 보입니다. 함수가 "Start of loop" 를 찍고 값을 내놓으면, 받는 쪽이 "value: 0" 을 찍고, 다시 함수로 돌아와 "End of loop" 를 찍습니다.
끝까지 돈 경우
for 를 다 돌고 "끝" 을 찍은 뒤 finally 가 실행됩니다.
yield break
i 가 2 일 때 목록을 그 자리에서 끝냅니다. 그 아래 "End of loop" 와 "끝" 은 건너뛰지만 finally 는 실행됩니다.
받는 쪽이 break
foreach 가 Dispose 를 불러 주기 때문에 이때도 finally 가 실행됩니다. 2단계의 using 이 하는 일입니다.
게임 UI 에서 쓰는 yield
public void Run()
{
Transform parent = GameObject.FindObjectOfType<Canvas>().transform;
UISystemMenu menuWindow = UIWindow<UISystemMenuElement>.Create<UISystemMenu>(parent);
for (int i = 0; i < 3; ++i)
menuWindow.AddElement(0);
IEnumerable<UISystemMenuElement> elements = menuWindow.GetElements(0);
int cnt = 1;
foreach (var element in menuWindow.GetElements(0))
{
element.SetMenuText("Menu" + cnt);
cnt++;
}
}메뉴 창의 요소 목록을 꺼내는 GetElements(0) 도 yield 로 만든 IEnumerable 입니다. 창 안쪽이 리스트로 되어 있든 사전으로 되어 있든, 쓰는 쪽은 foreach 하나로 돕니다. 이 UI 창 구조는 뒤의 UI 편에서 자세히 봅니다.
돌아보며
이번 편에서 얻은 것
- yield return 은 값을 하나 내놓고 멈췄다가 다음 요청 때 이어서 실행한다
- foreach 는 GetEnumerator, MoveNext, Current, Dispose 를 줄여 쓴 것이다
- 목록을 끝까지 돌지 않아도 finally 는 실행된다
다음에 풀 질문
- 멈췄다 이어 가는 성질로 "3초 기다렸다가 실행" 을 만들 수 있을까 (코루틴)
- Unity 는 IEnumerator 를 언제 MoveNext 하는가
정리
- yield return 은 값을 하나씩 내놓습니다. 함수는 멈췄다가, 다음 값을 달라고 할 때 이어서 실행합니다.
- foreach 는 MoveNext 와 Current 입니다. GetEnumerator 로 도구를 받고, MoveNext 가 false 일 때까지 Current 를 읽습니다.
- 값은 필요할 때 만들어집니다. 그래서 끝없는 목록도 만들 수 있고, 중간에 멈추면 finally 로 정리됩니다.
다음 편에서는 같은 IEnumerator 로 Unity 가 "기다렸다가 이어 가기" 를 하는 코루틴과, 진짜 동시에 도는 스레드를 비교합니다.