'수익률'의 적절한 사용


903

항복 키워드는 그 중 하나입니다 키워드 나 신비화 계속 C #에서, 나는 올바르게 사용하고 있음을 나는 확신 적이 없습니다.

다음 두 가지 코드 중 어떤 것이 선호되고 왜 그럴까요?

버전 1 : 수익률 사용

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        foreach (Product product in products)
        {
            yield return product;
        }
    }
}

버전 2 : 목록 반환

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList<Product>();
    }
}

38
yieldIEnumerable<T>그리고 그 종류에 묶여 있습니다. 그것은 어쨌든 게으른 평가에있다
— Jaider

다음은 비슷한 질문에 대한 답입니다. stackoverflow.com/questions/15381708/…
— Sanjeev Rai

1
다음은 좋은 사용 예입니다 : stackoverflow.com/questions/3392612/…
— ValGe

6
yield return결과를 반복하는 코드가 GetAllProducts()사용자에게 처리를 조기에 취소 할 수있는 경우를 사용하는 것이 좋습니다 .
— JMD

2
나는이 스레드가 정말 도움이 발견 programmers.stackexchange.com/a/97350/148944
— PiotrWolkowski에게

답변:


806

목록의 다음 항목 (또는 다음 항목 그룹)을 계산할 때 수익률 반환을 사용하는 경향이 있습니다.

버전 2를 사용하면 리턴하기 전에 전체 목록이 있어야합니다. 수익률을 사용하면 반품하기 전에 다음 품목 만 있으면됩니다.

무엇보다도 복잡한 계산의 계산 비용을 더 큰 시간 범위에 걸쳐 분산시키는 데 도움이됩니다. 예를 들어, 목록이 GUI에 연결되어 있고 사용자가 마지막 페이지로 이동하지 않으면 목록의 최종 항목을 계산하지 않습니다.

yield-return이 바람직한 다른 경우는 IEnumerable이 무한 세트를 나타내는 경우입니다. 소수 목록 또는 무한 난수 목록을 고려하십시오. 한 번에 전체 IEnumerable을 반환 할 수 없으므로 yield-return을 사용하여 목록을 증분 반환합니다.

특정 예에서는 전체 제품 목록이 있으므로 버전 2를 사용합니다.


31
나는 질문 3의 예에서 두 가지 이점을 돋보이게한다는 것을 nitpick하고 싶습니다. 1) 계산 비용을 분산시킵니다 (때로는 이점이 아니라 때로는 이점이 없습니다) 2) 많은 사용 사례에서 계산을 무기한으로 피할 수 있습니다. 중간 상태를 유지한다는 잠재적 인 단점은 언급하지 못했습니다. 많은 양의 중간 상태 (예 : 중복 제거를위한 HashSet)가있는 경우 yield를 사용하면 메모리 공간이 늘어날 수 있습니다.
— Kennet Belenky

8
또한 각 개별 요소가 매우 크지 만 순차적으로 액세스해야하는 경우 수율이 더 좋습니다.
— Kennet Belenky

2
그리고 마지막으로 ... 매우 직렬화 된 형태로 비동기 코드를 작성하기 위해 yield를 사용하는 약간 기발하지만 때로는 효과적인 기술이 있습니다.
— Kennet Belenky

12
큰 CSV 파일을 읽을 때 흥미로운 또 다른 예가 있습니다. 각 요소를 읽고 싶지만 종속성을 추출하고 싶습니다. IEnumerable <>을 반환하면 각 행을 반환하고 각 행을 개별적으로 처리 할 수 ​​있습니다. 10 Mb 파일을 메모리로 읽을 필요가 없습니다. 한 번에 한 줄만.
— Maxime Rouiller

1
Yield return자신의 사용자 정의 반복자 클래스 (구현 IEnumerator)를 작성하는 것의 축약 형 인 것 같습니다. 따라서 언급 된 이점은 사용자 정의 반복자 클래스에도 적용됩니다. 어쨌든, 두 구조는 중간 상태를 유지합니다. 가장 간단한 형태는 현재 객체에 대한 참조를 유지하는 것입니다.
— J. Ouwehand

641

임시 목록을 채우는 것은 전체 비디오를 다운로드하는 yield것과 같지만 사용하는 것은 해당 비디오를 스트리밍하는 것과 같습니다.


180
이 답변이 기술적 인 답변이 아니라는 것을 완벽하게 알고 있지만, 수익률 키워드를 이해할 때 수익률과 동영상 스트리밍의 유사점이 좋은 예라고 생각합니다. 이 주제에 대해 기술적 인 모든 것이 이미 언급되었으므로 "다른 말로"설명하려고했습니다. 기술적이지 않은 용어로 아이디어를 설명 할 수 없다는 커뮤니티 규칙이 있습니까?
— anar khalilov

13
나는 누가 당신에게 투표를했는지 또는 왜 (나는 그들이 의견을 말하고 싶었는지) 확신하지 못하지만, 그것이 비 기술적 전망에서 다소 설명한다고 생각합니다.
— senfo

22
여전히 개념을 파악하고 초점을 맞추는 데 도움이되었습니다.
— Tony

11
나는이 답변을 좋아하지만 질문에 대답하지는 않습니다.
— ANeves

73

를 사용해야 할 때를 이해하기위한 개념적인 예로서 yield, 메소드 ConsumeLoop()가 ProduceList()다음으로 리턴 / 수율 된 항목을 처리 한다고 가정 해 봅시다 .

void ConsumeLoop() {
    foreach (Consumable item in ProduceList())        // might have to wait here
        item.Consume();
}

IEnumerable<Consumable> ProduceList() {
    while (KeepProducing())
        yield return ProduceExpensiveConsumable();    // expensive
}

이 없으면 을 반환하기 전에 목록을 작성해야하므로 yield호출 ProduceList()시간이 오래 걸릴 수 있습니다.

//pseudo-assembly
Produce consumable[0]                   // expensive operation, e.g. disk I/O
Produce consumable[1]                   // waiting...
Produce consumable[2]                   // waiting...
Produce consumable[3]                   // completed the consumable list
Consume consumable[0]                   // start consuming
Consume consumable[1]
Consume consumable[2]
Consume consumable[3]

을 사용하면 yield"병렬로"정렬되어 재 ​​배열됩니다.

//pseudo-assembly
Produce consumable[0]
Consume consumable[0]                   // immediately Consume
Produce consumable[1]
Consume consumable[1]                   // consume next
Produce consumable[2]
Consume consumable[2]                   // consume next
Produce consumable[3]
Consume consumable[3]                   // consume next

마지막으로 이미 많은 사람들이 이미 제안했듯이 이미 완성 된 목록이 있으므로 버전 2를 사용해야합니다.


30

나는 이것이 오래된 질문이라는 것을 알고 있지만, yield 키워드를 창조적으로 사용하는 방법의 한 예를 제시하고 싶습니다. 나는 이 기술로부터 정말로 혜택을 받았다. 이 질문에 걸려 넘어지는 다른 사람에게 도움이되기를 바랍니다.

참고 : yield 키워드는 컬렉션을 만드는 또 다른 방법이라고 생각하지 마십시오. 생산 능력의 큰 부분은 호출 코드가 다음 값을 반복 할 때까지 메소드 또는 속성에서 실행이 일시 중지 된다는 사실 에 있습니다. 내 예는 다음과 같습니다.

yield 키워드 (Rob Eisenburg의 Caliburn.Micro coroutines 구현 과 함께 )를 사용하면 다음과 같이 웹 서비스에 대한 비동기 호출을 표현할 수 있습니다.

public IEnumerable<IResult> HandleButtonClick() {
    yield return Show.Busy();

    var loginCall = new LoginResult(wsClient, Username, Password);
    yield return loginCall;
    this.IsLoggedIn = loginCall.Success;

    yield return Show.NotBusy();
}

이 작업은 BusyIndicator를 켜고 웹 서비스에서 Login 메서드를 호출하고 IsLoggedIn 플래그를 반환 값으로 설정 한 다음 BusyIndicator를 다시 끕니다.

작동 방식은 다음과 같습니다. IResult에는 Execute 메서드와 Completed 이벤트가 있습니다. Caliburn.Micro는 IEnumerator를 HandleButtonClick ()에 대한 호출에서 가져와 Coroutine.BeginExecute 메서드에 전달합니다. BeginExecute 메소드는 IResults를 통해 반복을 시작합니다. 첫 번째 IResult가 반환되면 HandleButtonClick () 내에서 실행이 일시 중지되고 BeginExecute ()는 이벤트 처리기를 Completed 이벤트에 연결하고 Execute ()를 호출합니다. IResult.Execute ()는 동기 또는 비동기 작업을 수행 할 수 있으며 완료되면 Completed 이벤트를 발생시킵니다.

LoginResult는 다음과 같습니다.

public LoginResult : IResult {
    // Constructor to set private members...

    public void Execute(ActionExecutionContext context) {
        wsClient.LoginCompleted += (sender, e) => {
            this.Success = e.Result;
            Completed(this, new ResultCompletionEventArgs());
        };
        wsClient.Login(username, password);
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
    public bool Success { get; private set; }
}

이와 같은 것을 설정하고 실행을 단계별로 진행하여 상황을 관찰하는 것이 도움이 될 수 있습니다.

이것이 누군가를 돕기를 바랍니다! 수확량을 사용할 수있는 다른 방법을 탐색하는 것을 정말 즐겼습니다.


1
코드 샘플은 for 또는 foreach 블록의 yield OUTSIDE를 사용하는 방법에 대한 훌륭한 예입니다. 대부분의 예제는 이터레이터 내에서 수익률 반환을 보여줍니다. SO에 대한 질문을 할 때 매우 유용합니다. 반복자 외부에서 yield를 사용하는 방법!
— shelbypereira

yield이런 식 으로 사용 된 적이 없었 습니다. 비동기 / 대기 패턴을 에뮬레이트하는 우아한 방법 인 것 같습니다 (이것이 yield오늘 다시 쓰여지면 대신 사용될 것이라고 가정 합니다). yield이 질문에 대답 한 후 C #이 발전함에 따라 이러한 창의적 사용으로 인해 수년 동안 수익이 줄어들고 있음을 발견 했습니까 ? 아니면 여전히 이와 같은 현대적인 영리한 사용 사례를 제시하고 있습니까? 그렇다면 다른 흥미로운 시나리오를 공유 하시겠습니까?
— Lopsided

27

이것은 기괴한 제안처럼 보이지만 yield파이썬에서 발전기에 대한 프레젠테이션을 읽음으로써 C # 에서 키워드 를 사용하는 방법을 배웠습니다 . David M. Beazley의 http://www.dabeaz.com/generators/Generators.pdf . 프레젠테이션을 이해하기 위해 많은 Python을 알 필요는 없습니다. 발전기의 작동 방식뿐만 아니라 왜 신경 써야 하는지를 설명하는 데 매우 도움이되었습니다.


1
프레젠테이션은 간단한 개요를 제공합니다. C #에서 작동하는 방법에 대한 자세한 내용은 Ray Chen이 stackoverflow.com/a/39507/939250 의 링크에서 설명합니다. 첫 번째 링크는 수율 반환 방법 끝에 두 번째 암시 적 반환이 있음을 자세히 설명합니다.
— Donal Lafferty

18

수백만 개의 객체를 반복해야하는 알고리즘에 대해서는 수율 반환이 매우 강력 할 수 있습니다. 승차 공유를 위해 가능한 여행을 계산해야하는 다음 예를 고려하십시오. 먼저 가능한 여행을 생성합니다.

    static IEnumerable<Trip> CreatePossibleTrips()
    {
        for (int i = 0; i < 1000000; i++)
        {
            yield return new Trip
            {
                Id = i.ToString(),
                Driver = new Driver { Id = i.ToString() }
            };
        }
    }

그런 다음 각 여행을 반복하십시오.

    static void Main(string[] args)
    {
        foreach (var trip in CreatePossibleTrips())
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

yield 대신 List를 사용하는 경우 메모리에 백만 개의 객체를 할당해야하며 (~ 190mb)이 간단한 예제를 실행하려면 ~ 1400ms가 걸립니다. 그러나 yield를 사용하면 이러한 모든 임시 객체를 메모리에 넣을 필요가 없으며 알고리즘 속도가 훨씬 빨라집니다.이 예제는 메모리 소비없이 전혀 ~ 400ms 만 실행됩니다.


2
덮개 아래에서 수확량이란 무엇입니까? 나는 그것이 목록이라고 생각했을 것이므로 메모리 사용을 어떻게 향상시킬 것인가?
— 롤

1
@rolls yield는 상태 머신을 내부적으로 구현하여 적용됩니다. 다음 은 구현에 대해 자세히 설명하는 3 개의 자세한 MSDN 블로그 게시물 이 포함 된 SO 답변입니다 . Raymond Chen @ MSFT
— Shiva 작성

13

두 가지 코드는 실제로 두 가지 다른 일을하고 있습니다. 첫 번째 버전은 필요에 따라 멤버를 가져옵니다. 두 번째 버전은 작업을 시작 하기 전에 모든 결과를 메모리에로드 합니다.

이것에 대한 옳고 그른 대답은 없습니다. 어느 것이 바람직한지는 상황에 따라 다릅니다. 예를 들어, 쿼리를 완료해야하는 시간이 제한되어 있고 결과와 반 복합적인 작업을 수행해야하는 경우 두 번째 버전이 더 좋습니다. 그러나 특히이 코드를 32 비트 모드로 실행하는 경우 큰 결과 집합에주의하십시오. 이 방법을 수행 할 때 OutOfMemory 예외에 여러 번 물 렸습니다.

명심해야 할 핵심 사항은 다음과 같습니다. 차이점은 효율성입니다. 따라서 코드를 더 간단하게 만들고 프로파일 링 후에 만 ​​코드를 변경해야합니다.


11

수확량에는 두 가지 큰 용도가 있습니다

임시 컬렉션을 만들지 않고 사용자 지정 반복을 제공하는 데 도움이됩니다. (모든 데이터로드 및 반복)

스테이트 풀 반복을 수행하는 데 도움이됩니다. (스트리밍)

아래는 위의 두 가지 점을 지원하기 위해 전체 데모로 만든 간단한 비디오입니다.

http://www.youtube.com/watch?v=4fju3xcm21M


10

이것이 Chris Sells 가 C # 프로그래밍 언어의 문장에 대해 말한 것입니다 .

수율 반환 후 코드를 실행할 수 있다는 점에서 수율 반환이 return과 동일하지 않은 경우가 있습니다. 예를 들어, 첫 번째 리턴 이후의 코드는 실행할 수 없습니다.

    int F() {
return 1;
return 2; // Can never be executed
}

대조적으로 여기에서 첫 번째 수익률 반환 후 코드를 실행할 수 있습니다.

IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}

이것은 종종 if 문에서 나를 물었습니다.

IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}

이 경우 수익률 수익률이 수익률과 같이 "최종"이 아님을 기억하는 것이 도움이됩니다.


모호함을 줄이려면 할 수 있는가, 할 수 있는가? 첫 번째 수익률이 반환되고 두 번째 수익률을 실행하지 못할 수 있습니까?
— 조노 크로포드

@JohnoCrawford 두 번째 yield 문은 IEnumerable의 두 번째 / 다음 값이 열거 된 경우에만 실행됩니다. 예를 들어 F().Any()첫 번째 결과 만 열거하려고 시도하면 반환되지 않습니다. 일반적으로, IEnumerable yield실제로 트리거되지 않을 수 있기 때문에 프로그램 상태 변경 에 의존해서는 안됩니다
— Zac Faragher

8

제품 LINQ 클래스가 열거 / 반복에 비슷한 수율을 사용한다고 가정 할 때 첫 번째 버전은 반복 될 때마다 하나의 값만 생성하므로 더 효율적입니다.

두 번째 예는 ToList () 메서드를 사용하여 열거 자 / 반복자를 목록으로 변환하는 것입니다. 즉, 열거 자의 모든 항목을 수동으로 반복 한 다음 단순 목록을 반환합니다.


8

이것은 요점 외에도 다소 문제가 있지만 질문에 모범 사례가 표시되어 있으므로 계속 진행하여 2 센트를 넣습니다. 이 유형의 경우 속성으로 만들기를 선호합니다.

public static IEnumerable<Product> AllProducts
{
    get {
        using (AdventureWorksEntities db = new AdventureWorksEntities()) {
            var products = from product in db.Product
                           select product;

            return products;
        }
    }
}

물론 조금 더 보일러 플레이트이지만 이것을 사용하는 코드는 훨씬 깨끗해 보입니다.

prices = Whatever.AllProducts.Select (product => product.price);

vs

prices = Whatever.GetAllProducts().Select (product => product.price);

참고 : 작업을 수행하는 데 시간이 걸릴 수있는 방법에는이 작업을 수행하지 않습니다.


7

이건 어때?

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList();
    }
}

나는 이것이 훨씬 깨끗하다고 ​​생각합니다. 그래도 확인해야 할 VS2008이 없습니다. 어쨌든 Products가 IEnumerable을 구현하면 (foreach 문에서 사용되는 것처럼) 직접 반환합니다.


2
답변을 게시하는 대신 더 많은 정보를 포함하도록 OP를 수정하십시오.
— Brian Rasmussen

글쎄, 당신은 OP가 정확히 무엇을 의미하는지 말해
— 주어야

Original Post, 나는 가정합니다. 게시물을 편집 할 수 없으므로이 방법이 사용 된 것 같습니다.
— petr k.

5

이 경우 코드 2 버전을 사용했을 것입니다. 사용 가능한 전체 제품 목록이 있고이 메소드 호출의 "소비자"가 예상 한 것이므로 전체 정보를 호출자에게 다시 보내야합니다.

이 방법의 호출자가 한 번에 "하나의"정보를 요구하고 다음 정보의 소비가 온 디맨드 방식 인 경우, 수익률 반환을 사용하면 실행 명령이 호출자에게 반환되도록하는 것이 유리합니다. 정보 단위를 사용할 수 있습니다.

수율 반환을 사용할 수있는 몇 가지 예는 다음과 같습니다.

  1. 발신자가 한 번에 한 단계 씩 데이터를 기다리는 복잡한 단계별 계산
  2. GUI에서의 페이징-사용자가 마지막 페이지에 도달 할 수 없으며 현재 페이지에 서브 세트의 정보 만 공개해야합니다.

귀하의 질문에 대답하기 위해 버전 2를 사용했을 것입니다.


3

목록을 직접 반환하십시오. 혜택:

  • 더 명확 해
  • 이 목록은 재사용 할 수 있습니다. (반복자는 사실 이 아닙니다) 사실 이 아닙니다 . 감사합니다 Jon

목록의 끝까지 반복 할 필요가 없거나 끝이없는 반복자 (수율)를 사용해야합니다. 예를 들어, 클라이언트 호출은 일부 술어를 만족시키는 첫 번째 제품을 검색 할 것입니다. 이터레이터 사용을 고려할 수도 있습니다. 비록 이것이 좋은 예일 수도 있지만 더 좋은 방법이있을 것입니다. 기본적으로 전체 목록을 계산해야한다는 것을 미리 알고 있다면 미리하십시오. 그렇지 않다고 생각되면 반복기 버전 사용을 고려하십시오.


IEnumerator <T>가 아니라 IEnumerable <T>로 돌아 오는 것을 잊지 마십시오. GetEnumerator를 다시 호출 할 수 있습니다.
— Jon Skeet

사전에 전체 목록을 계산해야한다고해도 수익률 반환을 사용하는 것이 유리할 수 있습니다. 컬렉션에 수십만 개의 항목이 포함 된 경우를 예로들 수 있습니다.
— Val

1

yield return keyphrase는 특정 컬렉션에 대한 상태 시스템을 유지 관리하는 데 사용됩니다. CLR이 yield return keyphrase가 사용되는 곳이면 CLR은 해당 코드에 Enumerator 패턴을 구현합니다. 이 유형의 구현은 키워드가 없을 때해야 할 모든 유형의 배관에서 개발자를 도와줍니다.

개발자가 일부 콜렉션을 필터링하고 콜렉션을 반복 한 다음 새 콜렉션에서 해당 오브젝트를 추출한다고 가정하십시오. 이런 종류의 배관은 단조롭습니다.

에 대한 자세한 이 문서에서 여기 키워드 .


-4

yield 의 사용법은 키워드 return 과 비슷하지만 제너레이터 를 반환한다는 점만 다릅니다 . 그리고 제너레이터 객체는 한 번만 통과 합니다 .

수확량 에는 두 가지 이점이 있습니다.

  1. 이 값을 두 번 읽을 필요는 없습니다.
  2. 많은 자식 노드를 얻을 수 있지만 메모리에 모두 넣을 필요는 없습니다.

또 다른 명확한 설명이 도움이 될 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.