Para que é a palavra-chave yield usada em C#?
na pergunta Como posso expor apenas um fragmento do IList<> uma das respostas tinha o seguinte excerto de código:
IEnumerable<object> FilteredList()
{
foreach( object item in FullList )
{
if( IsItemInPartialList( item )
yield return item;
}
}
O que é que a palavra-chave yield faz lá? Já o vi referenciado em alguns lugares, e outra pergunta, mas ainda não descobri o que ele realmente faz. Estou habituado a pensar em rendimento no sentido de um fio ceder a outro, mas isso não parece relevante aqui.
16 answers
A maneira mais fácil de entender um código como este é escrever um exemplo, definir alguns pontos de paragem e vê o que acontece.
Tenta ultrapassar isto por 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;
}
Quando passar pelo exemplo, irá encontrar a primeira chamada para o inteiros() devolve 1. A segunda chamada retorna 2 e a linha "yield return 1" nã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);
}
}
}
}
}
Sem rendimento, todos os itens da colecção são criados de uma só vez:
class SomeData
{
public SomeData() { }
static public IEnumerable<SomeData> CreateSomeDatas()
{
return new List<SomeData> {
new SomeData(),
new SomeData(),
new SomeData()
};
}
}
O mesmo código usando o yield, devolve 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 os seus dados simplesmente precisa do primeiro item da coleção, o resto dos itens não serão criados.
O operador de rendimento permite a criação de itens como é exigido. Isso é bom. razão para usá-lo.
yield return
é utilizado com enumeradores. Em cada chamada de declaração de rendimento, o controle é devolvido ao chamador, mas garante que o estado do callee é mantido. Devido a isso, quando o chamador enumera o próximo elemento, ele continua a execução no método de callee a partir da declaração imediatamente após a Declaração yield
.
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
}
}
Também, o estado é mantido para cada enumeração. Suponha que tenho outra chamada para o método Fibs()
então o estado será reiniciado para ele.
Intuitivamente, a palavra-chave devolve um valor da função sem a deixar, ou seja, no seu exemplo de código, devolve o valor actual item
e depois retoma o ciclo. Mais formalmente, é usado pelo compilador para gerar código para um iterator . Iteradores são funções que devolvem IEnumerable
objectos. O MSDN tem vários artigos sobre eles.
Uma lista ou uma implementação de array carrega todos os itens imediatamente, enquanto a implementação yield fornece uma solução de execução diferida.
Na prática, é frequentemente desejável realizar a quantidade mínima de trabalho necessária para reduzir o consumo de recursos de uma aplicação. Por exemplo, podemos ter uma aplicação que processa milhões de registos de uma base de dados. Os seguintes benefícios podem ser alcançados quando usamos IEnumerable em uma execução diferida modelo baseado em tracção:- é provável que a escalabilidade, fiabilidade e previsibilidade melhorem, uma vez que o número de registos não afecta significativamente as necessidades de recursos da aplicação.
- é provável que o desempenho e a capacidade de resposta melhorem uma vez que o processamento pode começar imediatamente em vez de esperar que toda a colecção seja carregada em primeiro lugar.
- é provável que a recuperabilidade e a utilização melhorem desde o pedido. pode ser parado, iniciado, interrompido ou falhar. Apenas os itens em progresso serão perdidos em comparação com a pré-obtenção de todos os dados, onde apenas usando uma parte dos resultados foi realmente usado.
- o processamento contínuo é possível em ambientes onde se adicionam fluxos de carga de trabalho constantes.
Aqui está uma comparação entre construir uma coleção primeiro, como uma lista, em comparação com o uso de yield.
Exemplo Da 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();
}
consola Resultado
ContactListStore: a criar o contacto 1
ContactListStore: a criar o contacto 2
ContactListStore: criar o contacto 3
Pronto para iterar através da colecção.
Nota: toda a colecção foi carregada em memória sem sequer pedir 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();
}
Resultado Da Consola
Pronto para iterar através da colecçã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();
}
Resultado Da Consola
Pronto para iterar através da colecção
ContactYieldStore: criar contacto 1
Olá, Bob.
Boa! Apenas o primeiro contacto foi construído quando o o cliente "tirou" o item da colecção.
foreach
", mas a recolha dos itens na colecção é cara por alguma razão (como pesquisá-los numa base de dados), e muitas vezes não irá precisar de toda a colecção, então irá criar uma função que constrói a colecção um item de cada vez e devolve-o ao consumidor (que poderá então terminar o esforço da colecção mais cedo).
Pensa nisto. vai ao balcão da carne e compra meio quilo de fiambre fatiado. O carniceiro leva um presunto de 10 libras para trás, coloca-o na máquina de cortar, corta tudo, depois traz a pilha de fatias de volta para ti e mede um quilo dela. (Velha maneira). Com {[[1]}, o talhante traz a máquina de cortar para o balcão, e começa a cortar e" ceder " cada fatia para a balança até que ele mede 1 libra, em seguida, enrolá-lo para você e você está feito. O velho caminho pode ser melhor para o carniceiro (deixa-o organizar as suas máquinas da maneira que ele gosta), mas a nova maneira é claramente mais eficiente na maioria dos casos para o consumidor.
A palavra-chave yield
permite-lhe criar um IEnumerable<T>
na forma em um bloco iterador. Este bloco iterator suporta execução diferida {[[30]} e se você não está familiarizado com o conceito pode parecer quase mágico. No entanto, no final de contas, é apenas um código que executa sem truques estranhos.
foreach
. No entanto, um laço foreach
também é açúcar sintático. Então vocês são duas abstrações removidas do Código real, e é por isso que inicialmente pode ser difícil entender como tudo funciona em conjunto.
Assume que tens 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");
}
Blocos iteradores reais muitas vezes têm condições e loops, mas quando você verificar as condições e desenrolar os loops eles ainda acabam como yield
declarações intercaladas com outro código.
Para enumerar o bloco iterador a foreach
, utiliza-se o loop:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Aqui está a saída (sem surpresas aqui):
Begin 1 After 1 2 After 2 42 End
Tal como indicado acima foreach
é açúcar sintático:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
Numa tentativa de desvendar isto, criei um diagrama de sequência com as abstracções removidas:
A máquina de Estado gerada pelo compilador também implementa o enumerador, mas para tornar o diagrama mais claro Mostrei-os como exemplos separados. (Quando a máquina de Estado é enumerada a partir de outro tópico você realmente obter instâncias separadas, mas esse detalhe não é importante aqui.)
Cada vez que você chama o seu iterator bloqueia uma nova instância da máquina do Estado é criada. No entanto, nenhum do seu código no bloco iterator é executado atéenumerator.MoveNext()
executar pela primeira vez. É assim que a execução adiada funciona. Aqui está um exemplo (bastante tolo):
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
Neste momento, o iterator não executou. A cláusula Where
cria um novo {[7] } que envolve o IEnumerable<T>
devolvido por IteratorBlock
mas este enumerável ainda tem de ser enumerado. Isto acontece quando se executa um ciclo foreach
:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
Se você enumerar o enumerável duas vezes, então uma nova instância da máquina de Estado é criada de cada vez e seu bloco iterador irá executar o mesmo código duas vezes.
Reparem que métodos LINQ comoToList()
, ToArray()
, First()
, Count()
etc. irá usar um laço foreach
para enumerar o enumeravel. Por exemplo ToList()
irá enumerar todos os elementos do enumerável e armazená-los em uma lista. Agora você pode acessar a lista para obter todos os elementos do enumerável sem que o bloco iterator execute novamente. Há um trade-off entre usar CPU para produzir os elementos do enumerável múltiplas vezes e memória para armazenar os elementos da enumeração para acessá-los várias vezes ao usar métodos como ToList()
.
A Palavra-chave C# yield, para ser mais simples, permite muitas chamadas para um corpo de código, referido como iterador, que sabe como retornar antes de ser feito e, quando chamado novamente, continua onde parou - ou seja, ajuda um iterador a tornar-se claramente stateful por cada item em uma sequência que o iterator retorna em chamadas sucessivas.
Em JavaScript, o mesmo conceito é chamado de geradores.
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;
}
Notem que o rendimento não voltará do método. Você pode até colocar um WriteLine
Depois do yield return
O acima produz um IEnumerable de 4 ints 4,4,4,4
Aqui com um. Irá adicionar 4 à lista, imprimir abc, em seguida, adicionar 4 à lista, em seguida, completar o método e então realmente voltar do método(uma vez que o método tenha concluído, como aconteceria com um procedimento sem retorno). Mas isto teria um valor, uma listaIEnumerable
de int
s, que 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 elemento dentro da lista IEnumerable
.
Usa o yield com o tipo de retorno do método como IEnumerable
. Se o tipo de retorno do método for int
ou List<int>
e você usar yield
, Então ele não irá compilar. Pode utilizar IEnumerable
método de devolução SEM rendimento mas parece que talvez você não possa usar yield sem o tipo de retorno do método IEnumerable
.
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");
}
Se eu entender isto corretamente, aqui está como eu diria isto a partir da perspectiva da função implementando Ienumerável com yield.
- Aqui está uma.
Liga outra vez se precisares de outra.
Vou lembrar-me do que já te dei.
Só saberei se te posso dar outra quando voltares a ligar.
conceito: Este é um código de Ruby de amostra 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 Matriz de cada implementação do método produz o controle sobre o autor da chamada (o 'coloca-x') com cada elemento da matriz fossem apresentados como x. O chamador pode, então, fazer tudo o que ele precisa fazer com x.
No entanto . Net não vai até aqui.. C # parece ter acoplado o rendimento com IEnumerable, de certo modo 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];
}