Uso adequado do 'retorno do rendimento'

903

A palavra-chave yield é uma daquelas palavras-chave em C # que continuam me intrigando, e nunca tive certeza de que estou usando-a corretamente.

Dos dois pedaços de código a seguir, qual é o preferido e por quê?

Versão 1: Usando retorno de rendimento

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

Versão 2: retornar a lista

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

        return products.ToList<Product>();
    }
}
senfo
fonte
38
yieldestá ligado a IEnumerable<T>e seu tipo. É de alguma maneira avaliação preguiçosa
Jaider
aqui está uma grande resposta à pergunta semelhante. stackoverflow.com/questions/15381708/…
Sanjeev Rai
1
Aqui está um bom exemplo de uso: stackoverflow.com/questions/3392612/…
ValGe
6
Vejo um bom argumento para usar yield returnse o código que itera nos resultados de GetAllProducts()permite ao usuário a chance de cancelar prematuramente o processamento.
JMD 27/03
2
Eu encontrei esta discussão realmente útil: programmers.stackexchange.com/a/97350/148944
PiotrWolkowski

Respostas:

806

Costumo usar o retorno do rendimento quando calculo o próximo item da lista (ou mesmo o próximo grupo de itens).

Usando sua versão 2, você deve ter a lista completa antes de retornar. Ao usar o retorno do rendimento, você realmente só precisa do próximo item antes de retornar.

Entre outras coisas, isso ajuda a espalhar o custo computacional de cálculos complexos por um período maior. Por exemplo, se a lista estiver conectada a uma GUI e o usuário nunca for para a última página, você nunca calculará os itens finais na lista.

Outro caso em que o retorno do rendimento é preferível é se o IEnumerable representa um conjunto infinito. Considere a lista de números primos ou uma lista infinita de números aleatórios. Você nunca pode retornar o IEnumerable completo de uma só vez, portanto, use yield-return para retornar a lista incrementalmente.

No seu exemplo específico, você tem a lista completa de produtos, portanto, eu usaria a Versão 2.

abelenky
fonte
31
Eu diria que, no seu exemplo da pergunta 3, há dois benefícios. 1) Distribui o custo computacional (às vezes um benefício, às vezes não) 2) Pode preguiçosamente evitar o cálculo indefinidamente em muitos casos de uso. Você não menciona a desvantagem potencial que ela mantém em torno do estado intermediário. Se você tiver quantidades significativas de estado intermediário (por exemplo, um HashSet para eliminação duplicada), o uso do rendimento poderá aumentar o espaço ocupado pela memória.
Kennet Belenky
8
Além disso, se cada elemento individual for muito grande, mas eles precisarem ser acessados ​​sequencialmente, o rendimento será melhor.
19412 Kennet Belenky
2
E finalmente ... há uma técnica um pouco instável, mas ocasionalmente eficaz, para usar yield para escrever código assíncrono de uma forma muito serializada.
19412 Kennet Belenky
12
Outro exemplo que pode ser interessante é ao ler arquivos CSV bastante grandes. Você deseja ler cada elemento, mas também deseja extrair sua dependência. O rendimento retornando um IEnumerable <> permitirá retornar cada linha e processar cada linha individualmente. Não é necessário ler um arquivo de 10 Mb na memória. Apenas uma linha de cada vez.
precisa saber é o seguinte
1
Yield returnparece ser uma abreviação para escrever sua própria classe de iterador personalizado (implementar IEnumerator). Portanto, os benefícios mencionados também se aplicam às classes do iterador customizado. De qualquer forma, ambas as construções mantêm o estado intermediário. Na sua forma mais simples, trata-se de manter uma referência ao objeto atual.
J. Ouwehand
641

Preencher uma lista temporária é como baixar o vídeo inteiro, enquanto usar yieldé como transmitir esse vídeo.

anar khalilov
fonte
180
Estou perfeitamente ciente de que esta resposta não é uma resposta técnica, mas acredito que a semelhança entre rendimento e fluxo de vídeo serve como um bom exemplo ao entender a palavra-chave rendimento. Tudo técnico já foi dito sobre esse assunto, então tentei explicar "em outras palavras". Existe uma regra da comunidade que diz que você não pode explicar suas idéias em termos não técnicos?
anar Khalilov
13
Não tenho certeza de quem votou negativamente em você ou por quê (eu gostaria que eles tivessem comentado), mas acho que descreve um pouco de uma perspectiva não técnica.
senfo 8/07/13
22
Ainda compreender o conceito e isso ajudou a focar ainda mais, boa analogia.
21414 Tony
11
Eu gosto desta resposta, mas ela não responde à pergunta.
ANeves
73

Como um exemplo conceitual para entender quando você deve usar yield, digamos que o método ConsumeLoop()processe os itens retornados / produzidos por ProduceList():

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

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

Sem yield, a chamada para ProduceList()pode demorar muito, pois você precisa concluir a lista antes de retornar:

//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]

Usando yield, torna-se reorganizado, meio que trabalhando "em paralelo":

//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

E, finalmente, como muitos já sugeriram, você deve usar a versão 2, porque você já tem a lista completa de qualquer maneira.

Kache
fonte
30

Sei que essa é uma pergunta antiga, mas gostaria de oferecer um exemplo de como a palavra-chave yield pode ser usada de forma criativa. Eu realmente me beneficiei dessa técnica. Espero que isso ajude qualquer pessoa que tropeça nessa questão.

Nota: Não pense na palavra-chave yield como sendo apenas outra maneira de criar uma coleção. Uma grande parte do poder do rendimento vem do fato de a execução ser pausada em seu método ou propriedade até que o código de chamada itere sobre o próximo valor. Aqui está o meu exemplo:

O uso da palavra-chave yield (ao lado da implementação Caliburn.Micro coroutines de Rob Eisenburg ) permite expressar uma chamada assíncrona para um serviço da Web como este:

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

O que isso fará é ativar meu BusyIndicator, chamar o método Login no meu serviço da web, definir meu sinalizador IsLoggedIn com o valor de retorno e desativar o BusyIndicator.

Eis como isso funciona: IResult possui um método Execute e um evento Completed. Caliburn.Micro pega o IEnumerator da chamada para HandleButtonClick () e o passa para um método Coroutine.BeginExecute. O método BeginExecute começa a iterar pelos IResults. Quando o primeiro IResult é retornado, a execução é pausada dentro de HandleButtonClick () e BeginExecute () anexa um manipulador de eventos ao evento Completed e chama Execute (). IResult.Execute () pode executar uma tarefa síncrona ou assíncrona e dispara o evento Concluído quando terminar.

O LoginResult é mais ou menos assim:

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

Pode ajudar a configurar algo assim e avançar na execução para observar o que está acontecendo.

Espero que isso ajude alguém! Eu realmente gostei de explorar as diferentes maneiras pelas quais o rendimento pode ser usado.

Adam W. McKinley
fonte
1
seu exemplo de código é um excelente exemplo de como usar o rendimento FORA de um bloco for ou foreach. A maioria dos exemplos mostra retorno de rendimento em um iterador. Muito útil, pois eu estava prestes a fazer a pergunta sobre SO Como usar o rendimento fora de um iterador!
Shelbypereira 15/08/19
Nunca me ocorreu usar yielddessa maneira. Parece uma maneira elegante de emular o padrão async / waitit (que eu assumo que seria usado em vez de yieldse isso fosse reescrito hoje). Você descobriu que esses usos criativos yieldproduziram (sem trocadilhos) retornos decrescentes ao longo dos anos à medida que o C # evoluiu desde que você respondeu a essa pergunta? Ou você ainda está apresentando casos de uso inteligentes e modernizados como esse? E se sim, você se importaria de compartilhar outro cenário interessante para nós?
Lopsided
27

Isso vai parecer uma sugestão bizarra, mas eu aprendi como usar a yieldpalavra - chave em C # lendo uma apresentação sobre geradores em Python: David M. Beazley's http://www.dabeaz.com/generators/Generators.pdf . Você não precisa conhecer muito Python para entender a apresentação - eu não. Achei muito útil explicar não apenas como os geradores funcionam, mas por que você deveria se importar.

Robert Rossney
fonte
1
A apresentação fornece uma visão geral simples. Os detalhes de como ele funciona em C # são discutidos por Ray Chen nos links stackoverflow.com/a/39507/939250 O primeiro link explica em detalhes que há um segundo retorno implícito no final dos métodos de retorno de rendimento.
Donal Lafferty
18

O retorno do rendimento pode ser muito poderoso para algoritmos nos quais você precisa iterar através de milhões de objetos. Considere o exemplo a seguir, onde você precisa calcular possíveis viagens para compartilhamento de viagens. Primeiro, geramos possíveis viagens:

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

Em seguida, percorra cada viagem:

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

Se você usar Lista em vez de rendimento, precisará alocar 1 milhão de objetos na memória (~ 190mb) e este exemplo simples levará ~ 1400ms para executar. No entanto, se você usar yield, não precisará colocar todos esses objetos temporários na memória e obterá uma velocidade do algoritmo significativamente mais rápida: este exemplo levará apenas ~ 400ms para ser executado sem nenhum consumo de memória.

Andzej Maciusovic
fonte
2
sob as cobertas, o que é rendimento? Eu teria pensado que era uma lista, portanto, como isso iria melhorar o uso de memória?
rola
1
O @rolls yieldtrabalha sob a proteção implementando uma máquina de estado internamente. Aqui está uma resposta SO com 3 postagens detalhadas do blog do MSDN que explicam a implementação em detalhes. Escrito por Raymond Chen @ MSFT
Shiva
13

Os dois pedaços de código estão realmente fazendo duas coisas diferentes. A primeira versão puxará os membros conforme você precisar. A segunda versão carregará todos os resultados na memória antes de você começar a fazer alguma coisa.

Não há resposta certa ou errada para esta. Qual deles é preferível depende apenas da situação. Por exemplo, se houver um limite de tempo para concluir sua consulta e você precisar fazer algo semi-complicado com os resultados, a segunda versão poderá ser preferível. Mas tenha cuidado com grandes conjuntos de resultados, especialmente se você estiver executando esse código no modo de 32 bits. Fui mordido por exceções OutOfMemory várias vezes ao fazer esse método.

O principal a ter em mente é o seguinte: as diferenças estão na eficiência. Portanto, você provavelmente deve escolher o que simplificar seu código e alterá-lo somente após a criação de perfil.

Jason Baker
fonte
11

O rendimento tem dois grandes usos

Ajuda a fornecer iteração personalizada sem criar coleções temporárias. (carregando todos os dados e fazendo loop)

Ajuda a fazer iteração com estado. ( transmissão)

Abaixo está um vídeo simples que eu criei com demonstração completa para apoiar os dois pontos acima

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

Shivprasad Koirala
fonte
10

É o que Chris Sells conta sobre essas declarações na linguagem de programação C # ;

Às vezes esqueço que o retorno de rendimento não é o mesmo que retorno, pois o código após um retorno de rendimento pode ser executado. Por exemplo, o código após o primeiro retorno aqui nunca pode ser executado:

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

Por outro lado, o código após o primeiro retorno de rendimento aqui pode ser executado:

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

Isso geralmente me morde em uma declaração if:

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

Nesses casos, lembrar que o retorno do rendimento não é "final", como o retorno é útil.

Teoman shipahi
fonte
Para reduzir a ambiguidade, esclareça quando você diz que pode, é isso, será ou poderá? será possível que o primeiro retorne e não execute o segundo rendimento?
Johno Crawford
@JohnoCrawford a segunda declaração de rendimento será executada apenas se o segundo / próximo valor de IEnumerable for enumerado. É perfeitamente possível que isso não ocorra, por exemplo F().Any()- isso retornará depois de tentar enumerar apenas o primeiro resultado. Em geral, você não deve confiar em um IEnumerable yieldpara alterar o estado do programa, porque ele não pode realmente se desencadeou
Zac Faragher
8

Supondo que a classe LINQ de seus produtos use um rendimento semelhante para enumerar / iterar, a primeira versão é mais eficiente, pois apenas gera um valor por cada iteração.

O segundo exemplo é converter o enumerador / iterador em uma lista com o método ToList (). Isso significa que itera manualmente sobre todos os itens no enumerador e, em seguida, retorna uma lista simples.

Soviut
fonte
8

Isso está além do ponto, mas como a pergunta está marcada como boas práticas, vou adiante e coloco meus dois centavos. Para esse tipo de coisa, prefiro transformá-lo em uma propriedade:

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

            return products;
        }
    }
}

Claro, é um pouco mais resistente, mas o código que usa isso parecerá muito mais limpo:

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

vs

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

Nota: Eu não faria isso por nenhum método que demore um pouco para fazer seu trabalho.

Mark A. Nicolosi
fonte
7

E isso?

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

        return products.ToList();
    }
}

Eu acho que isso é muito mais limpo. Não tenho o VS2008 em mãos para verificar. Em qualquer caso, se Products implementa IEnumerable (como parece - é usado em uma instrução foreach), eu a retornaria diretamente.

petr k.
fonte
2
Edite o OP para incluir mais informações em vez de postar respostas.
Brian Rasmussen
Bem, você teria que me dizer o que OP significa exatamente :-) Obrigado
petr k.
Post original, eu assumo. Não consigo editar postagens, então esse parecia ser o caminho a seguir.
petr k.
5

Eu teria usado a versão 2 do código neste caso. Como você tem a lista completa de produtos disponíveis e é isso que o "consumidor" espera dessa chamada de método, seria necessário enviar as informações completas de volta ao chamador.

Se o chamador deste método exigir "uma" informação de cada vez e o consumo da próxima informação for sob demanda, seria benéfico usar o retorno de rendimento que garantirá que o comando de execução seja retornado ao chamador quando uma unidade de informação está disponível.

Alguns exemplos em que se poderia usar retorno de rendimento são:

  1. Cálculo complexo, passo a passo, em que o chamador aguarda os dados de uma etapa por vez
  2. Paginação na GUI - onde o usuário pode nunca chegar à última página e apenas um subconjunto de informações é necessário para ser divulgado na página atual

Para responder suas perguntas, eu teria usado a versão 2.

IntelligentBinary
fonte
3

Retorne a lista diretamente. Benefícios:

  • Está mais claro
  • A lista é reutilizável. (o iterador não é) não é verdade, obrigado Jon

Você deve usar o iterador (rendimento) a partir de quando achar que provavelmente não precisará repetir todo o caminho até o final da lista ou quando ele não tiver fim. Por exemplo, a chamada do cliente procurará o primeiro produto que satisfaça algum predicado, você pode considerar usar o iterador, embora esse seja um exemplo artificial e provavelmente haja maneiras melhores de realizá-lo. Basicamente, se você souber com antecedência que toda a lista precisará ser calculada, faça-a com antecedência. Se você acha que não, considere usar a versão do iterador.

recursivo
fonte
Não esqueça que está retornando em IEnumerable <T>, não em um IEnumerator <T> - você pode chamar GetEnumerator novamente.
Jon Skeet
Mesmo se você souber com antecedência que toda a lista precisará ser calculada, ainda pode ser benéfico usar o retorno do rendimento. Um exemplo é quando a coleção contém centenas de milhares de itens.
Val
1

A frase-chave de retorno de rendimento é usada para manter a máquina de estado para uma coleção específica. Sempre que o CLR vê a frase-chave de retorno de rendimento sendo usada, o CLR implementa um padrão de enumerador para esse trecho de código. Esse tipo de implementação ajuda o desenvolvedor de todo o tipo de canalização que, de outra forma, teríamos que fazer na ausência da palavra-chave.

Suponha que, se o desenvolvedor estiver filtrando alguma coleção, iterando pela coleção e extraindo esses objetos em alguma nova coleção. Esse tipo de encanamento é bastante monótono.

Mais sobre a palavra-chave aqui neste artigo .

Vikram
fonte
-4

O uso de yield é semelhante à palavra-chave return , exceto que ela retornará um gerador . E o objeto gerador irá percorrer apenas uma vez .

produção tem dois benefícios:

  1. Você não precisa ler esses valores duas vezes;
  2. Você pode obter muitos nós filhos, mas não precisa colocá-los na memória.

Há outra explicação clara que talvez o ajude.

123
fonte