Por que usar a palavra-chave yield, quando eu poderia usar apenas um IEnumerable comum?


171

Dado este código:

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

Por que não devo codificá-lo dessa maneira?

IEnumerable<object> FilteredList()
{
    var list = new List<object>(); 
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item ) )
            list.Add(item);
    }
    return list;
}

Eu meio que entendo o que a yieldpalavra - chave faz. Diz ao compilador para criar um certo tipo de coisa (um iterador). Mas por que usá-lo? Além de ser um pouco menos de código, o que isso faz por mim?


28
Sei que este é apenas um exemplo, mas realmente, o código deve ser escrito assim: FullList.Where(IsItemInPartialList):)
BlueRaja - Danny Pflughoeft

Respostas:


241

Usar yieldtorna a coleção preguiçosa.

Digamos que você só precise dos cinco primeiros itens. Do seu jeito, eu tenho que percorrer a lista inteira para obter os cinco primeiros itens. Com yield, eu apenas percorrer os cinco primeiros itens.


14
Observe que o uso FullList.Where(IsItemInPartialList)será tão preguiçoso. Somente, requer muito menos código personalizado --- gunk --- gerado pelo compilador. E menos tempo para o desenvolvedor escrever e manter. (Obviamente, esse foi apenas este exemplo) #
2828 deche

4
Esse é o Linq, não é? Eu imagino que o Linq faça algo muito semelhante debaixo das cobertas.
Robert Harvey

1
Sim, o Linq usou a execução atrasada ( yield return) sempre que possível.
Chad Schouggins

11
Não se esqueça que, se a instrução de retorno de rendimento nunca for executada, você ainda obterá um resultado de coleção vazio, portanto não será necessário se preocupar com uma exceção de referência nula. retorno de rendimento é impressionante com granulado de chocolate.
N

127

O benefício dos blocos de iteradores é que eles funcionam preguiçosamente. Então você pode escrever um método de filtragem como este:

public static IEnumerable<T> Where<T>(this IEnumerable<T> source,
                                   Func<T, bool> predicate)
{
    foreach (var item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

Isso permitirá que você filtre um fluxo pelo tempo que desejar, nunca armazenando em buffer mais do que um único item por vez. Se você só precisa do primeiro valor da sequência retornada, por exemplo, por que deseja copiar tudo para uma nova lista?

Como outro exemplo, você pode criar facilmente um fluxo infinito usando blocos iteradores. Por exemplo, aqui está uma sequência de números aleatórios:

public static IEnumerable<int> RandomSequence(int minInclusive, int maxExclusive)
{
    Random rng = new Random();
    while (true)
    {
        yield return rng.Next(minInclusive, maxExclusive);
    }
}

Como você armazenaria uma sequência infinita em uma lista?

Minha série de blogs Edulinq fornece uma implementação de exemplo do LINQ to Objects, que faz uso intenso de blocos de iteradores. O LINQ é fundamentalmente preguiçoso onde pode estar - e colocar as coisas em uma lista simplesmente não funciona dessa maneira.


1
Não tenho certeza se gostamos RandomSequenceou não do seu. Para mim, IEnumerable significa - primeiro e acima de tudo - que eu posso iterar com o foreach, mas isso obviamente levaria a um loop inifinito aqui. Eu consideraria isso um uso muito perigoso do conceito IEnumerable, mas YMMV.
Sebastian Negraszus

5
@SebastianNegraszus: Uma sequência de números aleatórios é logicamente infinita. Você poderia criar facilmente uma IEnumerable<BigInteger>representação da sequência de Fibonacci, por exemplo. Você pode usar foreachcom ele, mas nada sobre IEnumerable<T>garantias de que será finito.
precisa saber é o seguinte

42

Com o código "list", você precisa processar a lista completa antes de passar para a próxima etapa. A versão "yield" passa o item processado imediatamente para a próxima etapa. Se o "próximo passo" contiver um ".Take (10)", a versão "yield" processará apenas os 10 primeiros itens e esquecerá o resto. O código da "lista" teria processado tudo.

Isso significa que você vê a maior diferença quando precisa fazer muito processamento e / ou possui longas listas de itens a serem processados.


23

Você pode usar yieldpara retornar itens que não estão em uma lista. Aqui está uma pequena amostra que pode percorrer infinitamente uma lista até ser cancelada.

public IEnumerable<int> GetNextNumber()
{
    while (true)
    {
        for (int i = 0; i < 10; i++)
        {
            yield return i;
        }
    }
}

public bool Canceled { get; set; }

public void StartCounting()
{
    foreach (var number in GetNextNumber())
    {
        if (this.Canceled) break;
        Console.WriteLine(number);
    }
}

Isso escreve

0
1
2
3
4
5
6
7
8
9
0
1
2
3
4

... etc para o console até ser cancelado.


10
object jamesItem = null;
foreach(var item in FilteredList())
{
   if (item.Name == "James")
   {
       jamesItem = item;
       break;
   }
}
return jamesItem;

Quando o código acima é usado para percorrer FilteredList () e assumindo que item.Name == "James" será atendido no segundo item da lista, o método usando yield renderá duas vezes. Este é um comportamento preguiçoso.

Onde como o método using list adicionará todos os n objetos à lista e passará a lista completa ao método de chamada.

Este é exatamente um caso de uso em que a diferença entre IEnumerable e IList pode ser destacada.


7

O melhor exemplo do mundo real que eu já vi para o uso de yield seria calcular uma sequência de Fibonacci.

Considere o seguinte código:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(string.Join(", ", Fibonacci().Take(10)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(15).Take(1)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(10).Take(5)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(100).Take(1)));
        Console.ReadKey();
    }

    private static IEnumerable<long> Fibonacci()
    {
        long a = 0;
        long b = 1;

        while (true)
        {
            long temp = a;
            a = b;

            yield return a;

            b = temp + b;
        }
    }
}

Isso retornará:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55
987
89, 144, 233, 377, 610
1298777728820984005

Isso é bom porque permite calcular uma série infinita de maneira rápida e fácil, permitindo que você use as extensões do Linq e consulte apenas o que você precisa.


5
Não consigo ver nada do "mundo real" em um cálculo de sequência de Fibonacci.
Nek 25/10

Eu concordo que isso não é realmente "mundo real", mas que idéia legal.
Casey

1

por que usar [yield]? Além de ser um pouco menos de código, o que isso faz por mim?

Às vezes é útil, às vezes não. Se todo o conjunto de dados precisar ser examinado e retornado, não haverá nenhum benefício em usar o rendimento, porque tudo o que ele fez foi introduzir uma sobrecarga.

Quando o rendimento realmente brilha é quando apenas um conjunto parcial é retornado. Eu acho que o melhor exemplo é a classificação. Suponha que você tenha uma lista de objetos contendo uma data e uma quantia em dólares deste ano e gostaria de ver os primeiros (5) registros do ano.

Para conseguir isso, a lista deve ser classificada em ordem crescente por data e, em seguida, as 5 primeiras são obtidas. Se isso foi feito sem rendimento, toda a lista teria que ser classificada, para garantir que as duas últimas datas estivessem em ordem.

No entanto, com o rendimento, uma vez estabelecidos os 5 primeiros itens, a classificação é interrompida e os resultados estão disponíveis. Isso pode economizar uma grande quantidade de tempo.


0

A declaração de retorno de rendimento permite retornar apenas um item de cada vez. Você está coletando todos os itens em uma lista e retornando novamente a lista, que é uma sobrecarga de memória.

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.