[C#] yield

Kim Yuhyeon·2022년 3월 31일
0

게임개발

목록 보기
10/135

yield

yield 키워드는 호출자(Caller)에게 컬렉션 데이터를 하나씩 리턴할 때 사용한다. 흔히 Enumerator(Iterator)라고 불리우는 이러한 기능은
집합적인 데이터셋으로부터 데이타를 하나씩 호출자에게 보내주는 역할을 한다.

사용 방식

yield 는 yield return 또는 yield break의 2가지 방식으로 사용된다.

  • (1) yield return : 컬렉션 데이터를 하나씩 리턴하는데 사용
  • (2) yield break : 리턴을 중지하고 Iteration 루프를 빠져 나올 때 사용

예제

using System;
using System.Collections.Generic;

class Program
{
    static IEnumerable<int> GetNumber()
    {
        yield return 10;  // 첫번째 루프에서 리턴되는 값
        yield return 20;  // 두번째 루프에서 리턴되는 값
        yield return 30;  // 세번째 루프에서 리턴되는 값
    }

    static void Main(string[] args)
    {
        foreach (int num in GetNumber())
        {
            Console.WriteLine(num);
        }             
    }
}

GetNumber() 라는 메서드는 3개의 yield return 문을 가지고 있다.
만약 외부에서 이 GetNumber()를 호출하게 되면,
첫번째 호출시에는 첫번째 yield return 10 을 실행하여 10을 리턴하게 되고,
두번째로 호출되면yield return 20 이 실행되어 20을 리턴하게 된다.
이러한 방식으로 GetNumber()는 한꺼번에 10,20,30을 모두 리턴하는 것이 아니라,
한번 호출시마다 다음 yield return 문의 값을 리턴하는 것이다.

사용 사례

이러한 특별한 리턴 방식은 다음과 같은 경우에 유용하게 사용된다.

(1) 만약 데이터의 양이 커서 모든 데이터를 한꺼번에 리턴하는 것하는 것 보다 조금씩 리턴하는 것이 더 효율적일 경우.

어떤 검색에서 1만 개의 자료가 존재하는데,
UI에서 10개씩만 On Demand로 표시해 주는게 좋을 수도 있다.
즉, 사용자가 20개를 원할 지, 1000개를 원할 지 모르기 때문에,
일종의 지연 실행(Lazy Operation)을 수행하는 것이 나을 수 있다.

(2) 어떤 메서드가 무제한의 데이터를 리턴할 경우.

랜덤 숫자를 무제한 계속 리턴하는 함수는 결국 전체 리스트를 리턴할 수 없기 때문에 yield 를 사용해서 구현하게 된다.

(3) 모든 데이터를 미리 계산하면 속도가 느려서 그때 그때 On Demand로 처리하는 것이 좋은 경우.

소수(Prime Number)를 계속 리턴하는 함수의 경우,
소수 전체를 구하면 (물론 무제한의 데이터를 리턴하는 경우이기도 하지만)
시간상 많은 계산 시간이 소요되므로 다음 소수만 리턴하는 함수를 만들어
소요 시간을 분산하는 지연 계산(Lazy Calculation)을 구현할 수 있다.

yield와 Enumerator

yield 가 자주 사용되는 곳은 집합적 데이타를 가지고 있는 컬렉션 클래스이다.

일반적으로 컬렉션 클래스는 데이터 요소를 하나 하나 사용하기 위해 흔히 Enumerator(Iterator) 를 구현하는 경우가 많은데,
Enumerator를 구현하는 한 방법으로 yield 를 사용할 수 있다.

컬렉션 타입 혹은 Enumerable 클래스에서 GetEnumerator() 메서드를 구현하는 한 방법으로 yield 를 사용할 수 있다.

즉, GetEnumerator() 메서드에서 yield return를 사용하여
컬렉션 데이터를 순차적으로 하나씩 넘겨주는 코드를 구현하고,
리턴타입으로 IEnumerator 인터페이스를 리턴할 수 있다.

C#에서 Iterator 방식으로 yield 를 사용하면,
명시적으로 별도의 Enumerator 클래스를 작성할 필요가 없다.

예제

아래의 예제는 MyList라는 컬렉션 타입에 있는 데이터를 하나씩 리턴하는 GetEnumerator() 메서드의 샘플코드이다.

예제의 GetEnumerator() 메서드는 데이타를 하나씩 리턴하기 위해
yield return문을 while 루프 안에서 사용하고 있다.

클래스 안의 샘플 data는 1부터 5까지 숫자인데, 외부 호출자가 순차적으로 호출하면
yield return에서 하나씩 리턴한다.
(ex. 처음에는 1을 다음에는 2를, 그 다음에는 3을 리턴한다.)

호출자(Caller)가 이 메서드를 사용하는 방법은
(1) foreach 문을 사용하여 C#에서 자동으로 Iterator 루프 처리를 하게 하는 방법
(2) GetEnumerator()로부터 IEnumerator 인터페이스를 얻어 MoveNext() 메서드와 Current 속성을 사용하여 개발자가 직접 수동으로 요소를 하나씩 사용하는 방법

일반적으로 그 편리성 때문에 (1)번 방식을 사용한다.

아래 예제의 하단은
(1)foreach문 을 사용하여 Enumeration을 하는 방법과
(2) IEnumerator의 멤버를 써서 수동으로 Enumeration을 하는 방법을 예시하고 있다.

using System;
using System.Collections;

public class MyList
{
    private int[] data = { 1, 2, 3, 4, 5 };
    
    public IEnumerator GetEnumerator()
    {
        int i = 0;
        while (i < data.Length)
        {
            yield return data[i];
            i++;                
        }
    }

    //...
}

class Program
{
    static void Main(string[] args)
    {
        // (1) foreach 사용하여 Iteration
        var list = new MyList();

        foreach (var item in list)  
        {
            Console.WriteLine(item);
        }

        // (2) 수동 Iteration
        IEnumerator it = list.GetEnumerator();
        it.MoveNext();
        Console.WriteLine(it.Current);  // 1
        it.MoveNext();
        Console.WriteLine(it.Current);  // 2
    }
}

yield 실행 순서

C#에서 호출자가 yield를 가진 Iteration 메서드를 호출하면
다음과 같은 방식으로 실행된다.

즉, 호출자(A)가 IEnumerable을 리턴하는 메서드(B)를 호출하면,
yield return문에서 하나의 값을 리턴하고, 해당 메서드(B)의 위치를 기억해 둔다.

호출자(A)가 다시 루프를 돌아 다음 값을 메서드(B)에 요청하면,
메서드의 기억된 위치 다음 문장부터 실행하여 다음 yield 문을 만나 값을 리턴한다.

💡 참고 포스팅

[예제로 배우는 C# 프로그래미밍] 기초 문법 - C# yield

0개의 댓글