Um uso prático da palavra-chave "yield" em C # [fechado]


76

Após quase 4 anos de experiência, não vi um código em que a palavra-chave yield seja usada. Alguém pode me mostrar um uso prático (juntamente com a explicação) dessa palavra-chave e, se sim, não existem outras maneiras mais fáceis de cumprir o que ela pode fazer?


9
Todo (ou pelo menos a maioria) do LINQ é implementado usando rendimento. Além disso, o framework do Unity3D encontrou um bom uso para ele - é usado para pausar funções (em instruções de rendimento) e retomar mais tarde, usando o estado no IEnumerable.
30711 Dani

2
Isso não deve ser movido para StackOverflow?
Danny Varod

4
@ Danny - Não é adequado para o estouro de pilha, pois a pergunta não está pedindo para resolver um problema específico, mas sim sobre o que yieldpode ser usado em geral.
ChrisF

9
Sério? Não consigo pensar em um único aplicativo em que não o tenha usado.
Aaronaught 31/07

Respostas:


107

Eficiência

A yieldpalavra-chave cria efetivamente uma enumeração lenta sobre itens de coleção que podem ser muito mais eficientes. Por exemplo, se o foreachloop iterar apenas os 5 primeiros itens de 1 milhão de itens, tudo será yieldretornado e você não acumulou uma coleção de 1 milhão de itens internamente primeiro. Da mesma forma, você desejará usar yieldcom IEnumerable<T>valores de retorno em seus próprios cenários de programação para obter as mesmas eficiências.

Exemplo de eficiência obtida em um determinado cenário

Não é um método iterador, uso potencialmente ineficiente de uma grande coleção
(a coleção intermediária é criada com muitos itens)

// Method returns all million items before anything can loop over them. 
List<object> GetAllItems() {
    List<object> millionCustomers;
    database.LoadMillionCustomerRecords(millionCustomers); 
    return millionCustomers;
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in GetAllItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: One million items returned, but only 5 used. 

Versão do iterador, eficiente
(nenhuma coleção intermediária é criada)

// Yields items one at a time as the caller's foreach loop requests them
IEnumerable<object> IterateOverItems() {
    for (int i; i < database.Customers.Count(); ++i)
        yield return database.Customers[i];
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in IterateOverItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: Only 5 items were yielded and used out of the million.

Simplifique alguns cenários de programação

Em outro caso, facilita a programação de alguns tipos de classificação e mesclagem de listas, porque você apenas yieldretorna os itens na ordem desejada, em vez de ordená-los em uma coleção intermediária e trocá-los por lá. Existem muitos cenários desse tipo.

Apenas um exemplo é a mesclagem de duas listas:

IEnumerable<object> EfficientMerge(List<object> list1, List<object> list2) {
    foreach(var o in list1) 
        yield return o; 
    foreach(var o in list2) 
        yield return o;
}

Esse método retorna uma lista contígua de itens, efetivamente uma mesclagem sem nenhuma coleção intermediária necessária.

Mais informações

A yieldchave só pode ser utilizado em contexto de um método iteração (tendo um tipo de retorno IEnumerable, IEnumerator, IEnumerable<T>, ou IEnumerator<T>.) E existe uma relação especial com foreach. Iteradores são métodos especiais. A documentação de produção e o iterador do MSDN contêm muitas informações e explicações interessantes dos conceitos. Certifique-se de correlacioná-lo com a foreachpalavra - chave lendo sobre isso também, para complementar sua compreensão dos iteradores.

Para aprender sobre como os iteradores alcançam sua eficiência, o segredo está no código IL gerado pelo compilador C #. A IL gerada para um método iterador difere drasticamente da gerada para um método regular (não iterador). Este artigo (O que a palavra-chave Rendimento realmente gera?) Fornece esse tipo de insight.


2
Eles são particularmente úteis para algoritmos que pegam uma sequência (possivelmente longa) e geram outra onde o mapeamento não é individual. Um exemplo disso é o recorte de polígono; qualquer aresta particular pode gerar muitas ou mesmo nenhuma aresta uma vez cortada. Os iteradores tornam isso extremamente mais fácil de expressar e o rendimento é uma das melhores maneiras de escrevê-los.
Donal Fellows

+1 Resposta muito melhor como eu havia escrito. Agora também aprendi que o rendimento é bom para um melhor desempenho.
Jan_V

3
Era uma vez, usei o yield para criar pacotes para um protocolo de rede binário. Parecia a escolha mais natural em C #.
György Andrasek

4
Não database.Customers.Count()enumera toda a enumeração de clientes, exigindo, assim, que o código mais eficiente passe por cada item?
Stephen Stephen

5
Me chame de anal, mas isso é concatenação, não fusão. (E LINQ já tem um método Concat.)
OldFart

4

Há algum tempo, tive um exemplo prático, vamos supor que você tenha uma situação como esta:

List<Button> buttons = new List<Button>();
void AddButtons()
{
   for ( int i = 0; i <= 10; i++ ) {
      var button = new Button();
      buttons.Add(button);
      button.Click += (sender, e) => 
          MessageBox.Show(String.Format("You clicked button number {0}", ???));
   }
}

O objeto botão não conhece sua própria posição na coleção. A mesma limitação se aplica a Dictionary<T>ou outros tipos de coleções.

Aqui está minha solução usando a yieldpalavra-chave:

interface IHasId { int Id { get; set; } }

class IndexerList<T>: List<T>, IEnumerable<T> where T: IHasId
{
   List<T> elements = new List<T>();
   new public void Clear() { elements.Clear(); }
   new public void Add(T element) { elements.Add(element); }
   new public int Count { get { return elements.Count; } }    
   new public IEnumerator<T> GetEnumerator()
   {
      foreach ( T c in elements )
         yield return c;
   }

   new public T this[int index]
   {
      get
      {
         foreach ( T c in elements ) {
            if ( (int)c.Id == index )
               return c;
         }
         return default(T);
      }
   }
}

E é assim que eu uso:

class ButtonWithId: Button, IHasId
{
   public int Id { get; private set; }
   public ButtonWithId(int id) { this.Id = id; }
}

IndexerList<ButtonWithId> buttons = new IndexerList<ButtonWithId>();
void AddButtons()
{
   for ( int i = 10; i <= 20; i++ ) {
      var button = new ButtonWithId(i);
      buttons.Add(button);
      button.Click += (sender, e) => 
         MessageBox.Show(String.Format("You clicked button number {0}", ( (ButtonWithId)sender ).Id));
   }
}

Não preciso fazer um forloop na minha coleção para encontrar o índice. O My Button tem um ID e isso também é usado como índice IndexerList<T>, para evitar IDs ou índices redundantes - é disso que eu gosto! O índice / ID pode ser um número arbitrário.


2

Um exemplo prático pode ser encontrado aqui:

http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html

Há várias vantagens em usar o rendimento sobre o código padrão:

  • Se o iterador for usado para criar uma lista, você poderá obter o retorno e o chamador poderá decidir se deseja ou não esse resultado em uma lista.
  • O chamador também pode decidir cancelar a iteração por um motivo que está fora do escopo do que você está fazendo na iteração.
  • O código é um pouco menor.

No entanto, como Jan_V disse (apenas me derrote por alguns segundos :-) você pode viver sem ele, porque internamente o compilador produzirá código quase idêntico nos dois casos.


1

Aqui está um exemplo:

https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158

A turma realiza cálculos de datas com base em uma semana útil. Posso dizer a um exemplo da classe que Bob trabalha das 9:30 às 17:30 todos os dias da semana, com uma hora de intervalo para o almoço às 12:30. Com esse conhecimento, a função AscendingShifts () produzirá objetos de turno de trabalho entre as datas fornecidas. Para listar todos os turnos de trabalho de Bob entre 1º de janeiro e 1º de fevereiro deste ano, use-o assim:

foreach (var shift in week.AscendingShifts(new DateTime(2011, 1, 1), new DateTime(2011, 2, 1)) {
    Console.WriteLine(shift);
}

A classe realmente não interage com uma coleção. No entanto, as mudanças entre duas datas podem ser consideradas como uma coleção. O yieldoperador torna possível iterar sobre essa coleção imaginada sem criar a própria coleção.


1

Eu tenho uma pequena camada de dados db que possui uma commandclasse na qual você define o texto do comando SQL, o tipo de comando e retorna um IEnumerable de 'parâmetros de comando'.

Basicamente, a idéia é digitar comandos CLR em vez de preencher manualmente SqlCommandpropriedades e parâmetros o tempo todo.

Portanto, há uma função que se parece com isso:

IEnumerable<DbParameter> GetParameters()
{
    // here i do something like

    yield return new DbParameter { name = "@Age", value = this.Age };

    yield return new DbParameter { name = "@Name", value = this.Name };
}

A classe que herda essa commandclasse tem as propriedades Agee Name.

Em seguida, você pode criar um novo commandobjeto que preencha suas propriedades e passá-lo para uma dbinterface que realmente faz a chamada de comando.

Em suma, é realmente fácil trabalhar com comandos SQL e mantê-los digitados.


1

Embora o caso de fusão já tenha sido coberto na resposta aceita, deixe-me mostrar o método de extensão de parâmetros de produção e mesclagem ™:

public static IEnumerable<T> AppendParams<T>(this IEnumerable<T> a, params T[] b)
{
    foreach (var el in a) yield return el;
    foreach (var el in b) yield return el;
}

Eu uso isso para construir pacotes de um protocolo de rede:

static byte[] MakeCommandPacket(string cmd)
{
    return
        header
        .AppendParams<byte>(0, 0, 1, 0, 0, 1, 0x92, 0, 0, 0, 0)
        .AppendAscii(cmd)
        .MarkLength()
        .MarkChecksum()
        .ToArray();
}

O MarkChecksummétodo, por exemplo, se parece com isso. E tem yieldtambém:

public static IEnumerable<byte> MarkChecksum(this IEnumerable<byte> data, int pos = 6)
{
    foreach (byte b in data)
    {
        yield return pos-- == 0 ? (byte)data.Sum(z => z) : b;
    }
}

Mas tenha cuidado ao usar métodos agregados como Sum () em um método de enumeração, pois eles acionam um processo de enumeração separado.


1

O repositório de exemplos do Elastic Search .NET tem um ótimo exemplo de yield returncomo particionar uma coleção em várias coleções com um tamanho especificado:

https://github.com/elastic/elasticsearch-net-example/blob/master/src/NuSearch.Domain/Extensions/PartitionExtension.cs

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
    {
        T[] array = null;
        int count = 0;
        foreach (T item in source)
        {
            if (array == null)
            {
                array = new T[size];
            }
            array[count] = item;
            count++;
            if (count == size)
            {
                yield return new ReadOnlyCollection<T>(array);
                array = null;
                count = 0;
            }
        }
        if (array != null)
        {
            Array.Resize(ref array, count);
            yield return new ReadOnlyCollection<T>(array);
        }
    }

0

Expandindo a resposta de Jan_V, acabei de encontrar um caso do mundo real relacionado a ele:

Eu precisava usar as versões Kernel32 do FindFirstFile / FindNextFile. Você identifica a primeira chamada e a alimenta em todas as chamadas subseqüentes. Coloque isso em um enumerador e você obterá algo que você pode usar diretamente com o foreach.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.