c# unity Uso adequado de 'retorno de rendimento'




yield.net c# (13)

A palavra-chave yield é uma daquelas keywords em C # que continuam a me incomodar, e nunca confiei que estou usando corretamente.

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

Versão 1: usando o 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: retorne 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>();
    }
}

O rendimento tem dois grandes usos

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

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

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

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


Eu sei que esta é 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 seja útil para qualquer outra pessoa que se deparar com essa 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 que a execução é pausada em seu método ou propriedade até que o código de chamada repita o próximo valor. Aqui está o meu exemplo:

A utilização da palavra-chave yield (juntamente com a implementação Caliburn.Micro coroutines de Rob Eisenburg) permite-me 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 como o valor de retorno e, em seguida, desativar o BusyIndicator.

Veja como isso funciona: IResult tem um método de execução e um evento concluído. Caliburn.Micro pega o IEnumerator da chamada para HandleButtonClick () e 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 Concluído e chama Execute (). IResult.Execute () pode executar uma tarefa síncrona ou assíncrona e dispara o evento concluído quando ele é feito.

LoginResult é algo como isto:

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 passar pela execução para observar o que está acontecendo.

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


Supondo que seus produtos de classe LINQ usem um rendimento semelhante para enumerar / iterar, a primeira versão é mais eficiente, porque está produzindo apenas um valor a cada vez que é iterada.

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


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

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

Entre outras coisas, isso ajuda a espalhar o custo computacional de cálculos complexos em um período de tempo 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 yield-return é 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.

Em seu exemplo específico, você tem a lista completa de produtos, então eu usaria a versão 2.


O retorno de rendimento pode ser muito poderoso para algoritmos onde você precisa percorrer milhões de objetos. Considere o exemplo a seguir, no qual você precisa calcular possíveis viagens para o compartilhamento de viagens. Primeiro geramos viagens possíveis:

    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, faça uma iteração em cada viagem:

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

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


Devolva 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) de quando você pensa que você provavelmente não terá que iterar todo o caminho até o final da lista, ou quando não tem fim. Por exemplo, a chamada do cliente irá procurar o primeiro produto que satisfaça algum predicado, você pode considerar usar o iterador, embora seja um exemplo artificial, e provavelmente existem maneiras melhores de realizá-lo. Basicamente, se você souber de antemão que toda a lista precisará ser calculada, basta fazer isso de antemão. Se você acha que não, considere usar a versão do iterador.


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 espera do "consumidor" dessa chamada de método, seria necessário enviar as informações completas de volta ao chamador.

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

Alguns exemplos em que um poderia usar o retorno de rendimento é:

  1. Cálculo complexo, passo a passo, em que o chamador está aguardando dados de uma etapa de cada vez
  2. Paginação na GUI - onde o usuário nunca poderá acessar a última página e apenas o subconjunto de informações precisará ser divulgado na página atual

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


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

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

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


O keyphrase de retorno de rendimento é usado para manter a máquina de estado para uma coleção particular. Onde quer que o CLR veja o keyphrase de retorno de retorno sendo usado, o CLR implementa um padrão de Enumerador nessa parte do código. Esse tipo de implementação ajuda o desenvolvedor de todo o tipo de encanamento que, de outra forma, teríamos que fazer na ausência da palavra-chave.

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

Mais sobre a palavra-chave aqui neste artigo .


Preencher uma lista temporária é como fazer o download de todo o vídeo, enquanto usar o yield é como transmitir esse vídeo.


Isso é o que Chris Sells informa sobre essas instruções em The C # Programming Language ;

Às vezes, esqueço que o retorno do rendimento não é o mesmo que o retorno, em que 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
}

Em contraste, 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 de rendimento não é "final", como o retorno é útil.


Isso é meio que além do ponto, mas desde que a questão é marcada com as melhores práticas, eu vou em frente e jogo meus dois centavos. Para este tipo de coisa eu prefiro muito mais fazer 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 de caldeiraria, 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 possa demorar um pouco para fazer seu trabalho.


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







yield-return