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

73

Após quase 4 anos de experiência, não vi um código em que a palavra-chave yield fosse usada. Alguém pode me mostrar um uso prático (ao longo da explicação) dessa palavra-chave e, em caso afirmativo, não há outras maneiras mais fáceis de realizar o que ela pode fazer?

    
por Saeed Neamati 30.07.2011 / 20:01
fonte

8 respostas

103

Eficiência

A palavra-chave yield cria efetivamente uma enumeração lenta sobre itens de coleção que pode ser muito mais eficiente. Por exemplo, se o seu loop foreach percorrer apenas os primeiros 5 itens de 1 milhão de itens, então todos os yield retornam, e você não criou uma coleção de 1 milhão de itens internamente primeiro. Da mesma forma, você desejará usar yield com 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, potencial uso ineficiente de uma grande coleção,
(a coleção Intermediária é construída 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 em (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, é mais fácil programar alguns tipos de classificação e mesclagem de listas, porque você apenas yield itens retornam na ordem desejada, em vez de classificá-los em uma coleção intermediária e trocá-los lá. Existem muitos desses cenários.

Apenas um exemplo é a fusão 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;
}

Este método retorna uma lista contígua de itens, efetivamente uma mesclagem sem necessidade de coleta intermediária.

Mais informações

A palavra-chave yield só pode ser usada no contexto de um método iterador (com um tipo de retorno de IEnumerable , IEnumerator , IEnumerable<T> ou IEnumerator<T> .) e há uma relação especial com% código%. Iteradores são métodos especiais. A documentação de rendimento do MSDN e documentação do iterador contém muita informação interessante e explicação dos conceitos. Certifique-se de correlacioná-lo com a palavra-chave foreach da lendo sobre isso também, para complementar sua compreensão de iteradores.

Para aprender sobre como os iteradores alcançam sua eficiência, o segredo está no código IL gerado pelo compilador C #. O IL gerado para um método iterador difere drasticamente daquele gerado para um método regular (não-iterador). Este artigo (O que faz a palavra-chave de rendimento?) Really Generate?) fornece esse tipo de percepção.

    
por 31.07.2011 / 05:56
fonte
4

Algum tempo atrás eu tive um exemplo prático, vamos supor que você tenha uma situação como essa:

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 de 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á a minha solução usando a palavra-chave yield :

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

Eu não preciso fazer um loop for sobre minha coleção para encontrar o índice. Meu Button tem um ID e isso também é usado como índice em IndexerList<T> , então você evita quaisquer IDs ou índices redundantes - é disso que eu gosto! O índice / Id pode ser um número arbitrário.

    
por 09.06.2015 / 19:33
fonte
2

Um exemplo prático pode ser encontrado aqui:

link

Há várias vantagens de usar o rendimento em relação ao código padrão:

  • Se o iterador for usado para criar uma lista, você poderá gerar 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 esteja fora do escopo do que você está fazendo na iteração.
  • O código é um pouco mais curto.

No entanto, como Jan_V disse (apenas bata nele por alguns segundos :-) você pode viver sem ele porque internamente o compilador irá produzir código quase idêntico em ambos os casos.

    
por 30.07.2011 / 20:26
fonte
1

Veja um exemplo:

link

A turma realiza cálculos de data com base em uma semana de trabalho. Eu posso dizer um exemplo da classe que Bob trabalha das 9:30 às 17:30 todas as semanas 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, você o usaria assim:

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

A turma não é muito interessante sobre uma coleção. No entanto, os deslocamentos entre duas datas podem ser considerados como uma coleção. O operador yield possibilita iterar sobre essa coleção imaginada sem criar a própria coleção.

    
por 31.07.2011 / 12:28
fonte
1

Eu tenho uma pequena camada de dados db que tem uma classe command 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 é ter os comandos CLR digitados, em vez de preencher manualmente SqlCommand properties e parameters 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 classe command tem as propriedades Age e Name .

Em seguida, você pode adicionar um objeto command às suas propriedades e passá-lo para uma interface db que realmente faz a chamada de comando.

No geral, é muito fácil trabalhar com comandos SQL e mantê-los digitados.

    
por 31.07.2011 / 12:05
fonte
1

Embora o caso de mesclagem já tenha sido abordado na resposta aceita, deixe-me mostrar o método de extensão params de mesclagem de rendimento:

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 criar 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 método MarkChecksum , por exemplo, se parece com isso. E também tem um yield :

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.

    
por 10.06.2015 / 13:15
fonte
1

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

link

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);
        }
    }
    
por 10.09.2015 / 15:34
fonte
0

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

Eu precisava usar as versões do Kernel32 de FindFirstFile / FindNextFile. Você obtém uma alça da primeira chamada e a alimenta para todas as chamadas subsequentes. Embrulhe isso em um enumerador e você terá algo que você pode usar diretamente com foreach.

    
por 01.02.2015 / 17:32
fonte

Tags