본문 바로가기

IT 살이/04. 기술 - 프로그래밍

C#, yield return


※ 일단 "yield"는 "항복"보다는 "produce"라는 의미로 해석하자. 


C# 2.0에서 봤던 것 같은데, "yield return", 이런 녀석이 있구나 하고 그냥 넘어갔었다. 이 녀석을 다시 보게 된 것은 LINQ 때문이다. 그때 정리 좀 해야겠다 싶었던 LINQ의 "Deferred Execution"특성이 "yield return"과 연관되어 있다는 것을 느꼈다. yield return을 이해하면 도움이 될 것 같다는 생각을 했다. 


■ yield return 이란 뭣인가? 


지금까지 봐온 봐로는 yield return은 주로 컬렉션의 iterator를 구현할 때 이용하는 듯 하다. 다음과 같이 컬렉션이 있다고 해 보자.  


private static readonly string[] StringValues = new string[] { "The", "quick", "brown", "fox", 

                            "jumped", "over", "the", "lazy", "dog" };


이 컬렉션을 다음처럼 루핑하는 iterator를 만들고자 한다.


foreach(string v in TestIterator()) 

 Console.WriteLine("In foreach:{0}", v); 

}


그럼 다음과 같은 iterator를 만들면 된다.

static IEnumerable<string> TestIterator()

{

    foreach(string value in StringValues)

    {

        // 이곳에서 필요하면 value에 다른 가공할 수도 있다. 

        

         yield return value;

    }

}


yield return이 값을 반환하는 것까지는 일반 메소드와 다를 것이 없다. 그러나 C#에는 yield return을 만나면 "마지막 값을 반환한 위치(상태)를 기억"할 수 있는 메커니즘이 있다는 것이다. 위치를 기억해뒀다가 다음에 요청을 받으면 그때(on demand) 다음 요소를 반환해준다. 여기서 중요한 것은 요소에 대한 요청을 받으면 그때 반환해주는 특성이다. 

iterator의 반환값은 단순한 스칼라 값이 아니라 IEnuerable<T>이라는 것을 기억하자. 값을 반환하고 끝나는 것이 아니라는 점에서 return과 다르다. 다음 요소에 대한 요청을 받으면 그때 반환해 줄 요소에 대한 위치를 기억하고 있다는 것이다.

이런 특성으로 yield return은 위 예처럼 주로 foreach-ing에 사용된다. 

이처럼 클라이언트 코드에서 실제로 요청할때 값에 접근하는 것을 deferred execution이라고 한다. IEnueraable<T> 객체를 반환할때 컬렉션 객체 자체를 반환하는 것이 아니라, 컬렉션 생성기(Collection generator)를 반환하고 있는 것이다. 


■ 이런 기법은 왜 사용할까?


1) yield return의 장점은 컬렉션을 생성할 필요가 없다는 것이다. 때로 컬렉션을 생성하는 하는 작업은 비용이 많이 드는 경우가 있다. 클라이언트 코드가 요청할 때 요청하는 요소만 반환해도 되는 경우가 있다. 

yield return은 데이터 구조를 순회(traverse)하는 경우 다양한 방법을 제공한다. 만약 트리 구조라면, 리스트 객체를 만들지 않고도 pre- 또는 post 순으로 순회할 수 있다. 


public IEnumerable<T> InOrder()

{

    foreach (T k in kids)

        foreach (T n in k.InOrder())

            yield return n;

    yield return (T) this;

}


public IEnumerable<T> PreOrder()

{

    yield return (T) this;

    foreach (T k in kids)

        foreach (T n in k.PreOrder())

            yield return n;

}


2) 지연 시퀀스(lazy sequence)이란 것이 있는데, 이것은 yield return의 "on demand" 속성을 이용한다. 지연 시퀀스는 많은 장점이 있다. 

- 리소스가 많이 필요한 연산을 필요할 때까지 미룰 수 있다.

- 메모리 용량을 초과하는 시퀀스를 다룰 수 있다.

- I/O도 지연 시킬 수 있다.

이런 컨셉은 "함수적 프로그래밍(functional programming)"에서 사용된다.

LINQ도 "함수적 프로그래밍" 특성이 있다. 아래 링크에 가 보면 다음과 같은 샘플 코드가 있다.


public void DeferredExecution() 

{

 // Create an input sequence 

 var lstNumbers = new List() { 1, 2, 3 }; 

 // Create a query on the input sequence; 

 IEnumerable lst10Times = from number in lstNumbers select number * 10; 

 // Sneak another element in 

 lstNumbers.Add(4);

 // lst10Times is evaluated when enumerated and when constructed. This means that

 // the extra number sneaked in after the query was constructed is included in the result.

 PrintCollection(lst10Times);

}


select 같은 함수는 yield return를 사용하는 함수들이다. 실제로 요소에 대한 값을 요청할 때까지 값을 반환하지 않는다.


public static IEnumerable Select(this IEnumerable inputSequence, Func selector) 

{

 foreach (TSource element in inputSequence) 

 yield return selector(element);

}


그래서 PrintCollection(lst10Times);에서 "lst10Times"를 통해 요소의 값을 요청할때까지 값을 반환하지 않다. 그럼 lst10Times 변수의 값들을 출력하기 전에 lstNumbers.Add(4); 처럼 lstNumbers에 요소를 삽입하면 어떻게 될까? 결국 PrintCollection(lst10Times);에서는 4도 출력된다는 것이다. 


3) on demand 특성은 LINQ의 결과로 나온 컬렉션 변수를 원격으로 반환해서는 안된다는 의미이다. 즉 우리가 익숙한 layered style의 아키텍처에서 데이터베이스의 결과를 LINQ한 결과인 IEnumerable<T>를 클라이언트 단에 반환해 줄 수 없다는 의미이다. 


■ 참조

What is the purpose/advantage of using yield return iterators in C#?

http://stackoverflow.com/questions/1088442/what-is-the-purpose-advantage-of-using-yield-return-iterators-in-c


A closer look at yield – part 2

http://blogs.msdn.com/b/stuartleeks/archive/2008/07/15/a-closer-look-at-yield-part-2.aspx


LINQ

http://www.diranieh.com/NETCSharp/LINQ.htm


Iterator block implementation details: auto-generated state machines

http://blogs.msdn.com/b/stuartleeks/archive/2008/07/15/a-closer-look-at-yield-part-2.aspx