Em C #, por que um método anônimo não pode conter uma declaração de rendimento?


87

Achei que seria bom fazer algo assim (com o lambda fazendo um retorno de rendimento):

public IList<T> Find<T>(Expression<Func<T, bool>> expression) where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();

    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

Porém, descobri que não posso usar o yield no método anônimo. Estou me perguntando por quê. Os documentos de rendimento apenas dizem que não é permitido.

Como não era permitido, acabei de criar a lista e adicionar os itens a ela.


Agora que podemos ter asynclambdas anônimos permitindo o awaitinterior no C # 5.0, estou interessado em saber por que eles ainda não implementaram iteradores anônimos com o yieldinterior. Mais ou menos, é o mesmo gerador de máquina de estado.
noseratio

Respostas:


113

Eric Lippert escreveu recentemente uma série de posts sobre por que a produção não é permitida em alguns casos.

EDIT2:

  • Parte 7 (este foi postado mais tarde e trata especificamente desta questão)

Você provavelmente encontrará a resposta lá ...


EDIT1: isso é explicado nos comentários da Parte 5, na resposta de Eric ao comentário de Abhijeet Patel:

Q:

Eric,

Você também pode fornecer algumas dicas sobre por que "rendimentos" não são permitidos dentro de um método anônimo ou expressão lambda

UMA :

Boa pergunta. Eu adoraria ter blocos iteradores anônimos. Seria totalmente incrível ser capaz de construir você mesmo um pequeno gerador de sequência no local que fechasse as variáveis ​​locais. A razão pela qual não é simples: os benefícios não superam os custos. A grandiosidade de fazer geradores de sequência no local é realmente muito pequena no grande esquema das coisas e os métodos nominais fazem o trabalho bem o suficiente na maioria dos cenários. Portanto, os benefícios não são tão atraentes.

Os custos são altos. A reescrita do iterador é a transformação mais complicada no compilador, e a reescrita de método anônimo é a segunda mais complicada. Os métodos anônimos podem estar dentro de outros métodos anônimos, e os métodos anônimos podem estar dentro de blocos iteradores. Portanto, o que fazemos é primeiro reescrever todos os métodos anônimos para que se tornem métodos de uma classe de encerramento. Esta é a penúltima coisa que o compilador faz antes de emitir IL para um método. Depois que essa etapa for concluída, o reescritor do iterador pode assumir que não há métodos anônimos no bloco do iterador; todos eles já foram reescritos. Portanto, o reescritor do iterador pode apenas se concentrar em reescrever o iterador, sem se preocupar com a possibilidade de haver um método anônimo não realizado nele.

Além disso, os blocos iteradores nunca "aninham", ao contrário dos métodos anônimos. O reescritor do iterador pode assumir que todos os blocos do iterador são de "nível superior".

Se os métodos anônimos tiverem permissão para conter blocos de iteradores, ambas as suposições serão jogadas fora. Você pode ter um bloco iterador que contém um método anônimo que contém um método anônimo que contém um bloco iterador que contém um método anônimo e ... eca. Agora temos que escrever um passe de reescrita que pode lidar com blocos de iteradores aninhados e métodos anônimos aninhados ao mesmo tempo, mesclando nossos dois algoritmos mais complicados em um algoritmo muito mais complicado. Seria muito difícil projetar, implementar e testar. Somos inteligentes o suficiente para fazer isso, tenho certeza. Temos uma equipe inteligente aqui. Mas não queremos assumir esse grande fardo por um recurso "bom ter, mas não necessário". - Eric


2
Interessante, especialmente porque agora existem funções locais.
Mafii

4
Eu me pergunto se esta resposta está desatualizada porque levará um retorno de rendimento em uma função local.
Joshua

2
@Joshua mas uma função local não é o mesmo que um método anônimo ... retorno de rendimento ainda não é permitido em métodos anônimos.
Thomas Levesque

21

Eric Lippert escreveu uma excelente série de artigos sobre as limitações (e decisões de design que influenciam essas escolhas) nos blocos iteradores

Em particular, os blocos iteradores são implementados por algumas transformações de código do compilador sofisticadas. Essas transformações impactariam nas transformações que acontecem dentro de funções anônimas ou lambdas, de modo que, em certas circunstâncias, ambos tentariam 'converter' o código em alguma outra construção incompatível com a outra.

Como resultado, eles são proibidos de interação.

O modo como os blocos de iteradores funcionam nos bastidores é bem tratado aqui .

Como um exemplo simples de incompatibilidade:

public IList<T> GreaterThan<T>(T t)
{
    IList<T> list = GetList<T>();
    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

O compilador deseja simultaneamente converter isso para algo como:

// inner class
private class Magic
{
    private T t;
    private IList<T> list;
    private Magic(List<T> list, T t) { this.list = list; this.t = t;}

    public IEnumerable<T> DoIt()
    {
        var items = () => {
            foreach (var item in list)
                if (fun.Invoke(item))
                    yield return item;
        }
    }
}

public IList<T> GreaterThan<T>(T t)
{
    var magic = new Magic(GetList<T>(), t)
    var items = magic.DoIt();
    return items.ToList();
}

e, ao mesmo tempo, o aspecto do iterador está tentando fazer seu trabalho para fazer uma pequena máquina de estado. Certos exemplos simples podem funcionar com uma boa quantidade de verificação de sanidade (primeiro lidando com os fechamentos aninhados (possivelmente arbitrariamente)) e depois ver se as classes resultantes de nível inferior poderiam ser transformadas em máquinas de estado iterador.

No entanto, isso seria

  1. Muito trabalho.
  2. Não poderia funcionar em todos os casos sem, pelo menos, o aspecto do bloco iterador ser capaz de evitar que o aspecto do fechamento aplique certas transformações para eficiência (como promover variáveis ​​locais para variáveis ​​de instância em vez de uma classe de fechamento totalmente desenvolvida).
    • Se houvesse uma pequena chance de sobreposição onde fosse impossível ou suficientemente difícil de não ser implementado, o número de problemas de suporte resultantes provavelmente seria alto, uma vez que a alteração sutil seria perdida para muitos usuários.
  3. Pode ser facilmente contornado.

Em seu exemplo:

public IList<T> Find<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    return FindInner(expression).ToList();
}

private IEnumerable<T> FindInner<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();
    foreach (var item in list)
        if (fun.Invoke(item))
            yield return item;
}

2
Não há uma razão clara para que o compilador não possa, depois de remover todos os fechamentos, fazer a transformação de iterador usual. Você conhece algum caso que realmente apresentasse alguma dificuldade? Btw, sua Magicclasse deveria ser Magic<T>.
Qwertie

3

Infelizmente, não sei por que eles não permitiram isso, já que é perfeitamente possível imaginar como isso funcionaria.

No entanto, os métodos anônimos já são uma peça da "mágica do compilador", no sentido de que o método será extraído para um método na classe existente ou até mesmo para uma classe totalmente nova, dependendo se ela lida com variáveis ​​locais ou não.

Além disso, métodos iterativos usando yieldtambém são implementados usando a mágica do compilador.

Meu palpite é que um desses dois torna o código não identificável para a outra mágica e que foi decidido não perder tempo fazendo esse trabalho para as versões atuais do compilador C #. Claro, pode não ser uma escolha consciente, e simplesmente não funciona porque ninguém pensou em implementá-la.

Para uma pergunta 100% precisa, sugiro que você use o site Microsoft Connect e relate uma pergunta, tenho certeza de que você receberá algo utilizável em troca.


1

Eu faria isso:

IList<T> list = GetList<T>();
var fun = expression.Compile();

return list.Where(item => fun.Invoke(item)).ToList();

É claro que você precisa do System.Core.dll referenciado do .NET 3.5 para o método Linq. E inclui:

using System.Linq;

Felicidades,

Astuto


0

Talvez seja apenas uma limitação de sintaxe. No Visual Basic .NET, que é muito semelhante ao C #, é perfeitamente possível, embora seja difícil de escrever

Sub Main()
    Console.Write("x: ")
    Dim x = CInt(Console.ReadLine())
    For Each elem In Iterator Function()
                         Dim i = x
                         Do
                             Yield i
                             i += 1
                             x -= 1
                         Loop Until i = x + 20
                     End Function()
        Console.WriteLine($"{elem} to {x}")
    Next
    Console.ReadKey()
End Sub

Observe também os parênteses ' here; a função lambda Iterator Function... End Function retorna um IEnumerable(Of Integer)mas não é esse objeto em si. Deve ser chamado para obter esse objeto.

O código convertido por [1] gera erros em C # 7.3 (CS0149):

static void Main()
{
    Console.Write("x: ");
    var x = System.Convert.ToInt32(Console.ReadLine());
    // ERROR: CS0149 - Method name expected 
    foreach (var elem in () =>
    {
        var i = x;
        do
        {
            yield return i;
            i += 1;
            x -= 1;
        }
        while (!i == x + 20);
    }())
        Console.WriteLine($"{elem} to {x}");
    Console.ReadKey();
}

Eu discordo veementemente do motivo dado nas outras respostas que é difícil para o compilador manipular. O que Iterator Function()você vê no exemplo VB.NET é criado especificamente para iteradores lambda.

No VB, existe a Iteratorpalavra - chave; não tem contrapartida em C #. IMHO, não há razão real para que isso não seja um recurso do C #.

Portanto, se você realmente deseja funções iteradoras anônimas, use atualmente o Visual Basic ou (não o verifiquei) F #, conforme declarado em um comentário da Parte # 7 na resposta de @Thomas Levesque (use Ctrl + F para F #).

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.