Qual é a palavra-chave yield usada em C #?


828

Na pergunta Como posso expor apenas um fragmento de IList <>, uma das respostas tinha o seguinte trecho de código:

IEnumerable<object> FilteredList()
{
    foreach(object item in FullList)
    {
        if(IsItemInPartialList(item))
            yield return item;
    }
}

O que a palavra-chave yield faz lá? Eu já o vi referenciado em alguns lugares e em outra pergunta, mas ainda não descobri o que realmente faz. Estou acostumado a pensar em rendimento no sentido de um segmento ceder a outro, mas isso não parece relevante aqui.



14
Isto não é surpreendente. A confusão vem do fato de estarmos condicionados a ver "retorno" como uma saída de função, enquanto precedidos por um "rendimento" não é.
Larry

4
Eu li os documentos, mas tenho medo de ainda não entender :(
Ortund

Respostas:


737

A yieldpalavra-chave realmente faz bastante aqui.

A função retorna um objeto que implementa a IEnumerable<object>interface. Se uma função de chamada começar foreachsobre esse objeto, a função será chamada novamente até que "ceda". Este é o açúcar sintático introduzido no C # 2.0 . Nas versões anteriores, era necessário criar objetos IEnumerablee IEnumeratorobjetos próprios para fazer coisas assim.

A maneira mais fácil de entender códigos como esse é digitar um exemplo, definir alguns pontos de interrupção e ver o que acontece. Tente seguir este exemplo:

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

Ao seguir o exemplo, você encontrará a primeira chamada de Integers()retorno 1. A segunda chamada retorna 2e a linha yield return 1não é executada novamente.

Aqui está um exemplo da vida real:

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}

113
Nesse caso, seria mais fácil, estou apenas usando o número inteiro aqui para mostrar como o retorno de rendimento funciona. O bom de usar o retorno de rendimento é que é uma maneira muito rápida de implementar o padrão do iterador, para que as coisas sejam avaliadas preguiçosamente.
Mendelt

111
Também é importante notar que você pode usar yield break;quando não quiser mais retornar itens.
Rory

7
yieldnão é uma palavra-chave. Se fosse então não poderia usar rendimento como um identificador como emint yield = 500;
Brandin

5
@Brandin, porque todas as linguagens de programação suportam dois tipos de palavras-chave, a saber reservada e contextual. yield cai na categoria posterior, motivo pelo qual seu código não é proibido pelo compilador C #. Mais detalhes aqui: ericlippert.com/2009/05/11/reserved-and-contextual-keywords Você ficaria emocionado ao saber que também existem palavras reservadas que não são reconhecidas como palavras-chave por um idioma. Por exemplo, goto em java. Mais detalhes aqui: stackoverflow.com/questions/2545103/…
RBT

7
'If a calling function starts foreach-ing over this object the function is called again until it "yields"'. não parece certo para mim. Eu sempre pensei na palavra-chave c # yield no contexto de "a colheita produz uma colheita abundante", em vez de "o carro produz para o pedestre".
Zack

369

Iteração. Ele cria uma máquina de estado "embaixo das cobertas" que lembra onde você estava em cada ciclo adicional da função e parte daí.


210

O rendimento tem dois grandes usos,

  1. Ajuda a fornecer iteração personalizada sem criar coleções temporárias.

  2. Ajuda a fazer iteração com estado. insira a descrição da imagem aqui

Para explicar acima dois pontos mais demonstrativamente, eu criei um vídeo simples que você pode assistir aqui


13
O vídeo me ajuda a entender claramente o yield. Artigo do projeto de código de @ ShivprasadKoirala Qual é o uso do rendimento em C #? da mesma explicação é também uma boa fonte
Dush

Eu também adicionaria como um terceiro ponto que yieldé uma maneira "rápida" de criar um IEnumerator personalizado (em vez de uma classe implementar a interface do IEnumerator).
precisa saber é o seguinte

Eu assisti ao seu vídeo Shivprasad e ele explicou claramente o uso da palavra-chave yield.
Tore Aurstad

Obrigado pelo vídeo! Muito bem explicado!
Roblogic 25/11/19

Ótimo vídeo, mas imaginando ... A implementação usando yield é obviamente mais limpa, mas deve estar essencialmente criando sua própria memória temporária ou / e List internamente para acompanhar o estado (ou melhor, criar uma máquina de estado). Então, "Yield" está fazendo outra coisa senão tornar a implementação mais simples e fazer as coisas parecerem melhores ou há algo mais nisso? E quanto à eficiência, a execução de código usando o Yield é mais ou menos eficiente / rápida do que sem?
toughQuestions 19/03

135

Recentemente, Raymond Chen também publicou uma série interessante de artigos sobre a palavra-chave yield.

Embora seja nominalmente usado para implementar facilmente um padrão de iterador, pode ser generalizado em uma máquina de estado. Não faz sentido citar Raymond, a última parte também tem links para outros usos (mas o exemplo no blog de Entin é especialmente bom, mostrando como escrever código seguro assíncrono).


Isso precisa ser votado. Doce como ele explica o objetivo do operador e internos.
precisa saber é o seguinte

3
a parte 1 explica o açúcar sintático do "retorno do rendimento". excelente explicação!
Dror Weiss

99

À primeira vista, o retorno de rendimento é um açúcar .NET para retornar um IEnumerable .

Sem rendimento, todos os itens da coleção são criados de uma vez:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        return new List<SomeData> {
            new SomeData(), 
            new SomeData(), 
            new SomeData()
        };
    }
}

Mesmo código usando yield, ele retorna item por item:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        yield return new SomeData();
        yield return new SomeData();
        yield return new SomeData();
    }
}

A vantagem de usar yield é que, se a função que consome seus dados simplesmente precisar do primeiro item da coleção, o restante dos itens não será criado.

O operador de rendimento permite a criação de itens conforme exigido. Essa é uma boa razão para usá-lo.


40

yield returné usado com enumeradores. Em cada declaração de chamada de rendimento, o controle é retornado ao chamador, mas garante que o estado do chamado seja mantido. Devido a isso, quando o chamador enumera o próximo elemento, ele continua a execução no método chamado da instrução imediatamente após a yieldinstrução.

Vamos tentar entender isso com um exemplo. Neste exemplo, correspondendo a cada linha, mencionei a ordem na qual a execução flui.

static void Main(string[] args)
{
    foreach (int fib in Fibs(6))//1, 5
    {
        Console.WriteLine(fib + " ");//4, 10
    }            
}

static IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
    {
        yield return prevFib;//3, 9
        int newFib = prevFib + currFib;//6
        prevFib = currFib;//7
        currFib = newFib;//8
    }
}

Além disso, o estado é mantido para cada enumeração. Suponha que eu tenha outra chamada para o Fibs()método, em seguida, o estado será redefinido para ele.


2
defina prevFib = 1 - o primeiro número de Fibonacci é um "1", não um "0"
fubo 03/03

31

Intuitivamente, a palavra-chave retorna um valor da função sem deixá-lo, ou seja, no seu exemplo de código, ele retorna o itemvalor atual e, em seguida, retoma o loop. Mais formalmente, é usado pelo compilador para gerar código para um iterador . Iteradores são funções que retornam IEnumerableobjetos. O MSDN tem vários artigos sobre eles.


4
Bem, para ser mais preciso, ele não retoma o loop, ele o pausa até que o pai chame "iterator.next ()".
10283 Alex

8
@jitbit Foi por isso que usei “intuitivamente” e “mais formalmente”.
Konrad Rudolph

31

Uma implementação de lista ou matriz carrega todos os itens imediatamente, enquanto a implementação de rendimento fornece uma solução de execução adiada.

Na prática, geralmente é desejável executar a quantidade mínima de trabalho conforme necessário para reduzir o consumo de recursos de um aplicativo.

Por exemplo, podemos ter um aplicativo que processa milhões de registros de um banco de dados. Os seguintes benefícios podem ser alcançados quando usamos IEnumerable em um modelo baseado em pull de execução adiada:

  • É provável que a escalabilidade, a confiabilidade e a previsibilidade melhorem, pois o número de registros não afeta significativamente os requisitos de recursos do aplicativo.
  • É provável que o desempenho e a capacidade de resposta melhorem, pois o processamento pode começar imediatamente, em vez de esperar que toda a coleção seja carregada primeiro.
  • A recuperação e a utilização provavelmente melhorarão, pois o aplicativo pode ser parado, iniciado, interrompido ou falhar. Somente os itens em andamento serão perdidos em comparação com a pré-busca de todos os dados em que apenas uma parte dos resultados foi realmente usada.
  • O processamento contínuo é possível em ambientes onde fluxos de carga de trabalho constantes são adicionados.

Aqui está uma comparação entre criar uma coleção primeiro, como uma lista em comparação ao uso de yield.

Exemplo de lista

    public class ContactListStore : IStore<ContactModel>
    {
        public IEnumerable<ContactModel> GetEnumerator()
        {
            var contacts = new List<ContactModel>();
            Console.WriteLine("ContactListStore: Creating contact 1");
            contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
            Console.WriteLine("ContactListStore: Creating contact 2");
            contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
            Console.WriteLine("ContactListStore: Creating contact 3");
            contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
            return contacts;
        }
    }

    static void Main(string[] args)
    {
        var store = new ContactListStore();
        var contacts = store.GetEnumerator();

        Console.WriteLine("Ready to iterate through the collection.");
        Console.ReadLine();
    }

Saída do console
ContactListStore: Criando contato 1
ContactListStore: Criando contato 2
ContactListStore: Criando contato 3
Pronto para percorrer a coleção.

Nota: A coleção inteira foi carregada na memória sem solicitar um único item na lista

Exemplo de rendimento

public class ContactYieldStore : IStore<ContactModel>
{
    public IEnumerable<ContactModel> GetEnumerator()
    {
        Console.WriteLine("ContactYieldStore: Creating contact 1");
        yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
        Console.WriteLine("ContactYieldStore: Creating contact 2");
        yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
        Console.WriteLine("ContactYieldStore: Creating contact 3");
        yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
    }
}

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();

    Console.WriteLine("Ready to iterate through the collection.");
    Console.ReadLine();
}

Saída do console
Pronto para percorrer a coleção.

Nota: A coleção não foi executada. Isso ocorre devido à natureza de "execução adiada" do IEnumerable. A construção de um item só ocorrerá quando for realmente necessário.

Vamos chamar a coleção novamente e reverter o comportamento quando buscarmos o primeiro contato na coleção.

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();
    Console.WriteLine("Ready to iterate through the collection");
    Console.WriteLine("Hello {0}", contacts.First().FirstName);
    Console.ReadLine();
}

Saída do console
Pronta para percorrer a coleção
ContactYieldStore: Criando contato 1
Olá Bob

Agradável! Somente o primeiro contato foi construído quando o cliente "retirou" o item da coleção.


1
Esta resposta precisa de mais atenção! Thx
leon22

@ leon22 absolutamente +2
snr

26

Aqui está uma maneira simples de entender o conceito: A idéia básica é que, se você deseja usar uma coleção " foreach", mas reunir os itens na coleção é caro por algum motivo (como consultá-los em um banco de dados), E muitas vezes você não precisará da coleção inteira; em seguida, cria uma função que cria a coleção um item de cada vez e o devolve ao consumidor (que pode finalizar o esforço de coleta mais cedo).

Pense da seguinte maneira: você vai ao balcão de carne e quer comprar um quilo de presunto fatiado. O açougueiro pega um presunto de 10 libras nas costas, coloca-o na máquina de cortar fatias, corta a coisa toda, depois traz a pilha de fatias de volta para você e mede uma libra. (À moda antiga). Com ele yield, o açougueiro leva a máquina de fatiar para o balcão e começa a fatiar e "ceder" cada fatia na balança até medir 1 libra e depois embrulhá-la para você e pronto. O Old Way pode ser melhor para o açougueiro (permite que ele organize suas máquinas da maneira que ele gosta), mas o New Way é claramente mais eficiente na maioria dos casos para o consumidor.


18

A yieldpalavra-chave permite criar um IEnumerable<T>no formulário em um bloco iterador . Esse bloco iterador suporta execução adiada e, se você não estiver familiarizado com o conceito, pode parecer quase mágico. No entanto, no final do dia, é apenas um código que é executado sem truques estranhos.

Um bloco iterador pode ser descrito como açúcar sintático, em que o compilador gera uma máquina de estado que controla até que ponto a enumeração do enumerável progrediu. Para enumerar um enumerável, você costuma usar um foreachloop. No entanto, um foreachloop também é açúcar sintático. Portanto, você tem duas abstrações removidas do código real, e é por isso que inicialmente pode ser difícil entender como tudo funciona em conjunto.

Suponha que você tenha um bloco iterador muito simples:

IEnumerable<int> IteratorBlock()
{
    Console.WriteLine("Begin");
    yield return 1;
    Console.WriteLine("After 1");
    yield return 2;
    Console.WriteLine("After 2");
    yield return 42;
    Console.WriteLine("End");
}

Os blocos iteradores reais geralmente têm condições e loops, mas quando você verifica as condições e desenrola os loops, eles ainda acabam como yieldinstruções intercaladas com outro código.

Para enumerar o bloco iterador, um foreachloop é usado:

foreach (var i in IteratorBlock())
    Console.WriteLine(i);

Aqui está a saída (sem surpresas aqui):

Início
1
Após 1
2
Após 2
42.
Fim

Como afirmado acima, o foreachaçúcar sintático:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

Em uma tentativa de desembaraçar isso, criei um diagrama de sequência com as abstrações removidas:

Diagrama de sequência de blocos do iterador C #

A máquina de estado gerada pelo compilador também implementa o enumerador, mas para tornar o diagrama mais claro, eu os mostrei como instâncias separadas. (Quando a máquina de estado é enumerada de outro encadeamento, você obtém instâncias separadas, mas esse detalhe não é importante aqui.)

Toda vez que você chama seu bloco iterador, uma nova instância da máquina de estado é criada. No entanto, nenhum dos seus códigos no bloco iterador é executado até ser enumerator.MoveNext()executado pela primeira vez. É assim que a execução adiada funciona. Aqui está um exemplo (bastante bobo):

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

Neste ponto, o iterador não foi executado. A Wherecláusula cria um novo IEnumerable<T>que agrupa o IEnumerable<T>retornado por, IteratorBlockmas esse enumerável ainda não foi enumerado. Isso acontece quando você executa um foreachloop:

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

Se você enumerar o enumerável duas vezes, uma nova instância da máquina de estado será criada a cada vez e seu bloco iterador executará o mesmo código duas vezes.

Note-se que os métodos LINQ gosto ToList(), ToArray(), First(), Count()etc vai usar um foreachcircuito para enumerar o enumeráveis. Por exemplo ToList(), enumerará todos os elementos do enumerável e os armazenará em uma lista. Agora você pode acessar a lista para obter todos os elementos do enumerável sem que o bloco iterador seja executado novamente. Existe uma troca entre usar a CPU para produzir os elementos do enumerável várias vezes e a memória para armazenar os elementos da enumeração para acessá-los várias vezes ao usar métodos como ToList().


18

Se eu entendi isso corretamente, aqui está como eu fraseia isso da perspectiva da função implementando IEnumerable com yield.

  • Aqui está um.
  • Ligue novamente se precisar de outro.
  • Vou lembrar o que eu já te dei.
  • Só saberei se posso lhe dar outra quando ligar novamente.

simples e brilhante
Harry

10

A palavra-chave C # yield, para simplificar, permite muitas chamadas para um corpo de código, conhecido como iterador, que sabe retornar antes que seja concluído e, quando chamado novamente, continua onde parou - ou seja, ajuda um iterador torne-se transparente para cada item em uma sequência que o iterador retorna em chamadas sucessivas.

Em JavaScript, o mesmo conceito é chamado de Geradores.


Melhor explicação ainda. Estes também são os mesmos geradores em python?
petrosmm

7

É uma maneira muito simples e fácil de criar um enumerável para o seu objeto. O compilador cria uma classe que envolve seu método e que implementa, neste caso, IEnumerable <object>. Sem a palavra-chave yield, você teria que criar um objeto que implemente IEnumerable <object>.


5

Está produzindo uma enumerável sequência. Na verdade, o que ele faz é criar a sequência IEnumerable local e retorná-la como resultado do método


3

Este link tem um exemplo simples

Exemplos ainda mais simples estão aqui

public static IEnumerable<int> testYieldb()
{
    for(int i=0;i<3;i++) yield return 4;
}

Observe que o retorno do rendimento não retornará do método. Você pode até colocar um WriteLineapós oyield return

O exemplo acima produz um IEnumerable de 4 ints 4,4,4,4

Aqui com um WriteLine. Ele adicionará 4 à lista, imprimirá abc, depois adicionará 4 à lista, concluirá o método e realmente retornará do método (depois que o método for concluído, como aconteceria com um procedimento sem retorno). Mas isso teria um valor, uma IEnumerablelista de ints, que ele retorna após a conclusão.

public static IEnumerable<int> testYieldb()
{
    yield return 4;
    console.WriteLine("abc");
    yield return 4;
}

Observe também que, quando você usa yield, o que você está retornando não é do mesmo tipo que a função. É do tipo de um elemento dentro da IEnumerablelista.

Você usa yield com o tipo de retorno do método como IEnumerable. Se o tipo de retorno do método for intor List<int>e você usar yield, ele não será compilado. Você pode usar o IEnumerabletipo de retorno do método sem rendimento, mas parece que talvez não possa usar o rendimento sem o IEnumerabletipo de retorno do método.

E para executá-lo, é necessário chamá-lo de uma maneira especial.

static void Main(string[] args)
{
    testA();
    Console.Write("try again. the above won't execute any of the function!\n");

    foreach (var x in testA()) { }


    Console.ReadLine();
}



// static List<int> testA()
static IEnumerable<int> testA()
{
    Console.WriteLine("asdfa");
    yield return 1;
    Console.WriteLine("asdf");
}

nota- se tentar compreender SelectMany, ele usa o rendimento e também os genéricos .. Este exemplo pode ajudar public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { yield return t; }e public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { return new List<TResult>(); }
barlop

Parece uma explicação muito boa! Esta poderia ter sido a resposta aceita.
pongapundit

@pongapundit obrigado, minha resposta é certamente clara e simples, mas eu não usei muito, outros respondentes têm muito mais experiência e conhecimento de seus usos do que eu. O que eu escrevi sobre o rendimento aqui provavelmente foi arranhando minha cabeça tentando descobrir algumas das respostas aqui e no link dotnetperls! Mas como não conheço yield returnmuito bem (além da simples coisa que mencionei) e não o usei muito e não sei muito sobre seus usos, não acho que esse deva ser o aceito.
Barlop

3

Um ponto importante sobre a palavra-chave Yield é Lazy Execution . Agora, o que quero dizer com Lazy Execution é executar quando necessário. Uma maneira melhor de colocar isso é dando um exemplo

Exemplo: Não usar Rendimento, ou seja, Sem Execução Preguiçosa.

        public static IEnumerable<int> CreateCollectionWithList()
        {
            var list =  new List<int>();
            list.Add(10);
            list.Add(0);
            list.Add(1);
            list.Add(2);
            list.Add(20);

            return list;
        }

Exemplo: usando Yield, ou seja, Lazy Execution.

    public static IEnumerable<int> CreateCollectionWithYield()
    {
        yield return 10;
        for (int i = 0; i < 3; i++) 
        {
            yield return i;
        }

        yield return 20;
    }

Agora, quando eu chamo os dois métodos.

var listItems = CreateCollectionWithList();
var yieldedItems = CreateCollectionWithYield();

você notará que os itens da lista terão 5 itens (passe o mouse sobre os itens da lista durante a depuração). Enquanto yieldItems terá apenas uma referência ao método e não aos itens. Isso significa que ele não executou o processo de obter itens dentro do método. Uma maneira muito eficiente de obter dados somente quando necessário. A implementação real do rendimento pode ser vista no ORM, como Entity Framework e NHibernate, etc.


-3

Ele está tentando trazer alguma Ruby Goodness :)
Conceito: Este é um exemplo de código Ruby que imprime cada elemento da matriz

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with x
    }

A implementação de cada método da matriz gera controle sobre o chamador (o 'puts x'), com cada elemento da matriz claramente apresentado como x. O chamador pode fazer o que for necessário com x.

No entanto, o .Net não chega até aqui. O C # parece ter associado o rendimento ao IEnumerable, de certa forma forçando você a escrever um loop foreach no chamador, como visto na resposta de Mendelt. Um pouco menos elegante.

//calling code
foreach(int i in obCustomClass.Each())
{
    Console.WriteLine(i.ToString());
}

// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
   for(int iLooper=0; iLooper<data.Length; ++iLooper)
        yield return data[iLooper]; 
}

7
-1 Esta resposta não parece correta para mim. Sim, o C # yieldé associado IEnumerablee o C # não possui o conceito Ruby de "bloco". Mas o C # tem lambdas, o que poderia permitir a implementação de um ForEachmétodo, muito parecido com o de Ruby each. Isso não significa que seria uma boa ideia fazê-lo .
R17na #

Melhor ainda: public IEnumerable <int> Each () {int index = 0; dados de retorno de rendimento [index ++]; }
ata
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.