unity - yield.net c#




Uso adequado de 'retorno de rendimento' (11)

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

Como um exemplo conceitual para entender quando você deve usar o yield , digamos que o método ConsumeLoop() processa 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, porque 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 o yield , torna-se rearranjado, tipo de trabalho "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 por último, como muitos já sugeriram, você deve usar a Versão 2 porque você já tem a lista completa de qualquer maneira.


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.


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. Eu não tenho VS2008 na mão para verificar, no entanto. Em qualquer caso, se Products implementa IEnumerable (como parece - ele é usado em uma instrução foreach), eu o retornaria diretamente.


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.


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 é 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.


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 .


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


O uso do rendimento é semelhante ao retorno da palavra-chave, exceto que ele retornará um generator . E o objeto gerador só irá percorrer uma vez .

O rendimento 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 todos na memória.

Há outra explanation clara que pode ajudá-lo.


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.


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.





yield-return