C #에서 yield return iterator를 사용하는 목적 / 장점은 무엇입니까?


80

yield return x;C # 메서드 내부에서 사용한 모든 예제 는 전체 목록을 반환하는 방식으로 동일한 방식으로 수행 할 수 있습니다. 이러한 경우 사용에 따른 이점이나 이점이 있습니까?yield return 구문 보다 목록을 반환하는 있습니까?

또한 yield return전체 목록을 반환 할 수없는 시나리오 유형은 무엇 입니까?


15
처음에 "목록"이 있다고 가정하는 이유는 무엇입니까? 없는 경우 어떻게합니까?
Eric Lippert

2
@Eric, 그게 내가 요청한 것 같아요. 처음에 목록이 없을 때는 언제입니까? 파일 스트림과 무한 시퀀스는 지금까지 답변의 두 가지 훌륭한 예입니다.
CoderDennis

1
목록이 있다면, 그냥 돌려주세요. 그러나 메서드 내부에 목록을 만들고이를 반환하는 경우 대신 반복자를 사용할 수 있습니다. 항목을 한 번에 하나씩 양보하십시오. 많은 이점이 있습니다.
justin.m.chase

2
5 년 전이 질문을 한 이후로 확실히 많은 것을 배웠습니다!
CoderDennis 2014 년

1
yields 의 가장 큰 장점은 다른 중간 변수의 이름을 지정할 필요가 없다는 것입니다.
nawfal

답변:


120

하지만 컬렉션을 직접 만들고 있다면 어떨까요?

일반적으로 반복자는 일련의 객체느리게 생성하는 데 사용할 수 있습니다 . 예를 들어 Enumerable.Range메서드에는 내부적으로 어떤 종류의 컬렉션도 없습니다. 요청시 다음 숫자 생성합니다 . 상태 머신을 사용하는이 지연 시퀀스 생성에는 많은 용도가 있습니다. 대부분은 함수형 프로그래밍 개념으로 다룹니다. 집니다.

제 생각에는 컬렉션을 열거하는 방법으로 반복자를보고 있다면 (가장 간단한 사용 사례 중 하나 일뿐입니다) 잘못된 방향으로 가고있는 것입니다. 내가 말했듯이 반복자는 시퀀스를 반환하는 수단입니다. 시퀀스는 무한 할 수도 있습니다 . 무한한 길이의 목록을 반환하고 처음 100 개 항목을 사용할 수있는 방법은 없습니다. 그것은 때때로 지연 될 수 있습니다. 컬렉션 반환은 컬렉션 생성기 (반복자) 를 반환하는 것과 상당히 다릅니다 . 사과와 오렌지를 비교하고 있습니다.

가상의 예 :

static IEnumerable<int> GetPrimeNumbers() {
   for (int num = 2; ; ++num) 
       if (IsPrime(num))
           yield return num;
}

static void Main() { 
   foreach (var i in GetPrimeNumbers()) 
       if (i < 10000)
           Console.WriteLine(i);
       else
           break;
}

이 예제는 10000 미만의 소수를 인쇄합니다. 소수 생성 알고리즘을 전혀 건드리지 않고도 백만 미만의 숫자를 인쇄하도록 쉽게 변경할 수 있습니다. 이 예에서는 시퀀스가 ​​무한하고 소비자가 처음부터 원하는 항목 수조차 알지 못하기 때문에 모든 소수 목록을 반환 할 수 없습니다.


권리. 목록을 작성했지만 한 번에 하나의 항목을 반환하는 것과 전체 목록을 반환하는 것은 어떤 차이가 있습니까?
CoderDennis

4
다른 이유 중에서도 코드를 모듈화하여 항목을로드하고 처리 한 다음 반복 할 수 있습니다. 또한 항목을로드하는 데 비용이 많이 들거나 항목이 많은 경우 (백만 명)를 고려하십시오. 이러한 경우 전체 목록을로드하는 것은 바람직하지 않습니다.
Dana the Sane

15
@Dennis : 메모리에 선형으로 저장된 목록의 경우 차이가 없을 수 있지만 예를 들어 10GB 파일을 열거하고 각 줄을 하나씩 처리하면 차이가있을 것입니다.
mmx

1
+1 훌륭한 답변-yield 키워드를 사용하면 네트워크 소켓, 웹 서비스 또는 동시성 문제와 같이 전통적으로 컬렉션으로 간주되지 않는 소스에 반복기 의미론을 적용 할 수 있다고 덧붙입니다 ( stackoverflow.com/questions/ 참조). 481714 / ccr-yield-and-vb-net )
LBushkin 2009-07-06

좋은 예입니다. 기본적으로 컨텍스트 (예 : 메서드 호출)를 기반으로하고 무언가 액세스를 시도 할 때까지 작동하지 않는 컬렉션 생성기입니다. 반면 yield가없는 전통적인 컬렉션 메서드는 빌드 할 크기를 알아야합니다. 전체 컬렉션을 반환합니다. 그런 다음 해당 컬렉션의 필수 부분을 반복 하시겠습니까?
Michael Harper

24

여기에 좋은 답변이의 혜택을 제안 yield return것입니다 당신이 목록을 만들 필요가 없습니다 ; 목록은 비쌀 수 있습니다. (또한 잠시 후에 부피가 크고 우아하지 않은 것을 알게 될 것입니다.)

하지만 목록이 없다면 어떨까요?

yield return여러 가지 방법으로 데이터 구조 (목록 일 필요는 없음) 를 순회 할 수 있습니다 . 예를 들어 개체가 트리 인 경우 다른 목록을 만들거나 기본 데이터 구조를 변경하지 않고 사전 또는 사후 순서로 노드를 순회 할 수 있습니다.

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;
}

1
이 예에서는 위임 사례도 강조합니다. 특정 상황에서 다른 컬렉션의 항목을 포함 할 수있는 컬렉션이있는 경우 모든 결과의 전체 목록을 작성하고 반환하는 대신 yield return을 반복하고 사용하는 것이 매우 간단합니다.
Tom Mayfield

1
이제 C # yield!은 모든 foreach문이 필요하지 않도록 F #이 수행 하는 방식 을 구현 하면 됩니다.
CoderDennis 2012

부수적으로, 귀하의 예제는 다음의 "위험"중 하나를 보여줍니다 yield return. 효율적이거나 비효율적 인 코드를 생성하는시기가 명확하지 않은 경우가 많습니다. yield return재귀 적으로 사용할 수 있지만 이러한 사용은 깊게 중첩 된 열거 자의 처리에 상당한 오버 헤드를 부과합니다. 수동 상태 관리는 코딩하기가 더 복잡 할 수 있지만 훨씬 더 효율적으로 실행됩니다.
supercat

17

지연 평가 / 지연된 실행

"yield return"반복자 블록은 특정 결과를 실제로 호출 할 때까지 코드를 실행 하지 않습니다 . 즉, 효율적으로 함께 연결할 수 있습니다. 팝 퀴즈 : 다음 코드가 파일에서 몇 번 반복됩니까?

var query = File.ReadLines(@"C:\MyFile.txt")
                            .Where(l => l.Contains("search text") )
                            .Select(l => int.Parse(l.SubString(5,8))
                            .Where(i => i > 10 );

int sum=0;
foreach (int value in query) 
{
    sum += value;
}

정답은 정확히 하나이며, foreach루프가 내려 가기 전까지는 아닙니다 . 별도의 linq 연산자 함수가 세 개 있어도 파일 내용을 한 번만 반복합니다.

이것은 성능 이외의 이점이 있습니다. 예를 들어, 로그 파일을 한 번 읽고 사전 필터링 하는 상당히 간단하고 일반적인 방법을 작성하고 동일한 방법을 여러 다른 위치에서 사용할 수 있으며 각 사용시 다른 필터에 추가됩니다. 따라서 코드를 효율적으로 재사용하면서 좋은 성능을 유지합니다.

무한 목록

좋은 예는이 질문에 대한 내 대답을 참조하십시오
.C # 피보나치 함수가 오류를 반환합니다.

기본적으로 절대 멈추지 않을 반복자 블록 (적어도 MaxInt에 도달하기 전에는 안 됨)을 사용하여 피보나치 시퀀스를 구현 한 다음 해당 구현을 안전한 방식으로 사용합니다.

개선 된 의미 및 관심사 분리

위의 파일 예제를 다시 사용하면 이제 파일을 읽는 코드를 실제로 결과를 구문 분석하는 코드에서 불필요한 줄을 필터링하는 코드에서 쉽게 분리 할 수 ​​있습니다. 특히 첫 번째는 매우 재사용이 가능합니다.

이것은 단순한 시각적으로 누구보다 산문으로 설명하기가 훨씬 더 어려운 것들 중 하나입니다 1 :

문제의 명령 적 및 기능적 분리

이미지를 볼 수없는 경우 동일한 코드의 두 가지 버전이 표시되며 다른 관심사에 대한 배경이 강조 표시됩니다. linq 코드에는 모든 색상이 멋지게 그룹화되어 있지만 전통적인 명령형 코드에는 색상이 혼합되어 있습니다. 저자는이 결과가 linq를 사용하는 것보다 명령형 코드를 사용하는 것의 전형이라고 주장합니다. linq가 섹션간에 더 나은 흐름을 갖도록 코드를 더 잘 구성한다고 주장합니다.


1 나는 이것이 원본 출처라고 믿는다 : https://twitter.com/mariofusco/status/571999216039542784 . 또한이 코드는 Java이지만 C #은 비슷합니다.


1
지연된 실행은 아마도 반복자의 가장 큰 이점 일 것입니다.
justin.m.chase

12

반환해야하는 시퀀스가 ​​너무 커서 메모리에 맞지 않는 경우가 있습니다. 예를 들어, 약 3 개월 전에 MS SLQ 데이터베이스 간 데이터 마이그레이션 프로젝트에 참여했습니다. 데이터를 XML 형식으로 내보냈습니다. 수익률 반환XmlReader에서 매우 유용합니다 . 프로그래밍이 훨씬 쉬워졌습니다. 예를 들어 파일에 1000 개의 Customer 요소 가 있다고 가정합니다. 이 파일을 메모리로 읽어 들인 경우 순차적으로 처리 되더라도 모든 요소를 ​​동시에 메모리에 저장해야합니다. 따라서 컬렉션을 하나씩 탐색하기 위해 반복기를 사용할 수 있습니다. 이 경우 하나의 요소에 대한 메모리 만 사용해야합니다.

결과적 으로 우리 프로젝트에 XmlReader 를 사용 하는 것이 응용 프로그램을 작동시키는 유일한 방법이었습니다. 오랫동안 작동했지만 적어도 전체 시스템이 중단되지 않았고 OutOfMemoryException을 발생 시키지 않았습니다 . 물론 yield iterator없이 XmlReader로 작업 할 수 있습니다 . 그러나 반복기는 내 삶을 훨씬 더 쉽게 만들었습니다 (불필요하고 문제없이 가져 오기위한 코드를 작성하지 않을 것입니다). 수율 반복기가 실제 문제를 해결하는 데 어떻게 사용되는지 알아 보려면 이 페이지 를보십시오 (무한 시퀀스의 과학적뿐만 아니라).


9

장난감 / 데모 시나리오에서는 큰 차이가 없습니다. 그러나 산출 반복자가 유용한 상황이 있습니다. 때로는 전체 목록을 사용할 수 없거나 (예 : 스트림) 목록이 계산적으로 비싸고 전체적으로 필요하지 않을 수 있습니다.


2

전체 목록이 거대하다면 그냥 앉아서 많은 메모리를 먹을 수 있지만, 수확량을 사용하면 항목 수에 관계없이 필요할 때 필요한 것만 가지고 놀 수 있습니다.



2

를 사용하면 yield return목록을 작성할 필요없이 항목을 반복 할 수 있습니다. 목록이 필요하지 않지만 일부 항목 집합을 반복하려면 작성하는 것이 더 쉬울 수 있습니다.

foreach (var foo in GetSomeFoos()) {
    operate on foo
}

보다

foreach (var foo in AllFoos) {
    if (some case where we do want to operate on foo) {
        operate on foo
    } else if (another case) {
        operate on foo
    }
}

yield return을 사용하여 메서드 내에서 foo에 대해 작동할지 여부를 결정하는 모든 논리를 넣을 수 있으며 foreach 루프는 훨씬 더 간결 할 수 있습니다.


2

다음은 정확히 동일한 질문에 대한 이전 허용 답변입니다.

수익 키워드 값이 추가 되었습니까?

반복기 메서드를 보는 또 다른 방법은 알고리즘을 "안쪽으로"바꾸는 힘든 작업을 수행하는 것입니다. 파서를 고려하십시오. 스트림에서 텍스트를 가져 와서 패턴을 찾고 콘텐츠에 대한 높은 수준의 논리적 설명을 생성합니다.

이제 SAX 방식을 사용하여 파서 작성자로서이 작업을 쉽게 수행 할 수 있습니다. SAX 접근 방식에는 패턴의 다음 부분을 찾을 때마다 알리는 콜백 인터페이스가 있습니다. 따라서 SAX의 경우 요소의 시작을 찾을 때마다 beginElement메서드를 호출하는 식 입니다.

그러나 이것은 내 사용자에게 문제를 일으 킵니다. 핸들러 인터페이스를 구현해야하므로 콜백 메소드에 응답하는 상태 머신 클래스를 작성해야합니다. 이것은 제대로하기 어렵 기 때문에 가장 쉬운 방법은 DOM 트리를 구축하는 스톡 구현을 사용하는 것입니다. 그러면 그들은 트리를 걸을 수있는 편리함을 갖게 될 것입니다. 그러나 전체 구조가 메모리에 버퍼링됩니다. 좋지 않습니다.

하지만 대신 파서를 반복기 메서드로 작성하는 것은 어떻습니까?

IEnumerable<LanguageElement> Parse(Stream stream)
{
    // imperative code that pulls from the stream and occasionally 
    // does things like:

    yield return new BeginStatement("if");

    // and so on...
}

콜백 인터페이스 접근 방식보다 작성하기가 더 어렵지 않습니다 LanguageElement. 콜백 메서드를 호출하는 대신 기본 클래스 에서 파생 된 객체를 반환하기 만하면 됩니다.

이제 사용자는 foreach를 사용하여 파서의 출력을 반복 할 수 있으므로 매우 편리한 명령형 프로그래밍 인터페이스를 얻을 수 있습니다.

그 결과 사용자 정의 API의 양쪽이 모두 제어중인 것처럼 보이 므로 작성하고 이해하기가 더 쉽습니다.


2

yield를 사용하는 기본적인 이유는 자체적으로 목록을 생성 / 반환하기 때문입니다. 추가 반복을 위해 반환 된 목록을 사용할 수 있습니다.


개념적으로는 정확하지만 기술적으로는 올바르지 않습니다. 단순히 반복자를 추상화하는 IEnumerable의 인스턴스를 반환합니다. 이 반복자는 실제로 구체화 된 목록이 아니라 다음 항목을 가져 오는 논리입니다. 를 사용하면 return yield목록이 생성되지 않고, 요청 (반복) 될 때만 목록의 다음 항목이 생성됩니다.
Sinaesthetic
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.