Por que o retorno do rendimento não pode aparecer dentro de um bloco try com uma captura?

95

O seguinte está certo:

try
{
    Console.WriteLine("Before");

    yield return 1;

    Console.WriteLine("After");
}
finally
{
    Console.WriteLine("Done");
}

O finallybloco é executado quando a execução de toda a coisa termina ( IEnumerator<T>oferece IDisposableuma maneira de garantir isso mesmo quando a enumeração é abandonada antes de terminar).

Mas isso não está bem:

try
{
    Console.WriteLine("Before");

    yield return 1;  // error CS1626: Cannot yield a value in the body of a try block with a catch clause

    Console.WriteLine("After");
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Suponha (para fins de argumentação) que uma exceção seja lançada por uma ou outra das WriteLinechamadas dentro do bloco try. Qual é o problema em continuar a execução em catchbloco?

Obviamente, a parte do retorno do rendimento é (atualmente) incapaz de lançar qualquer coisa, mas por que isso deveria nos impedir de ter um fechamento try/ catchpara lidar com exceções lançadas antes ou depois de um yield return?

Atualização: Há um comentário interessante de Eric Lippert aqui - parece que eles já têm problemas suficientes para implementar o comportamento try / finally corretamente!

EDIT: A página MSDN sobre este erro é: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx . Mas não explica o porquê.

Daniel Earwicker
fonte
2
Link direto para o comentário de Eric Lippert: blogs.msdn.com/oldnewthing/archive/2008/08/14/…
Roman Starkov
observação: você também não pode ceder no bloco catch :-(
Simon_Weaver
2
O link oldnewthing não funciona mais.
Sebastian Redl

Respostas:

50

Suspeito que seja mais uma questão de praticidade do que de viabilidade. Suspeito que haja muito, muito poucas vezes em que essa restrição é realmente um problema que não pode ser contornado - mas a complexidade adicional no compilador seria muito significativa.

Existem algumas coisas assim que eu já encontrei:

  • Os atributos não podem ser genéricos
  • Incapacidade de X derivar de XY (uma classe aninhada em X)
  • Blocos de iterador usando campos públicos nas classes geradas

Em cada um desses casos, seria possível ganhar um pouco mais de liberdade, à custa de complexidade extra no compilador. A equipe fez a escolha pragmática, pela qual os aplaudo - prefiro uma linguagem um pouco mais restritiva com um compilador 99,9% preciso (sim, há bugs; encontrei um no SO outro dia) do que mais linguagem flexível que não pode ser compilada corretamente.

EDIT: Aqui está uma pseudo-prova de como isso é viável.

Considere isso:

  • Você pode ter certeza de que a parte do retorno do rendimento em si não lança uma exceção (calcule previamente o valor, e então você está apenas configurando um campo e retornando "verdadeiro")
  • Você tem permissão para tentar / pegar, que não usa retorno de rendimento em um bloco iterador.
  • Todas as variáveis ​​locais no bloco iterador são variáveis ​​de instância no tipo gerado, então você pode mover livremente o código para novos métodos

Agora transforme:

try
{
    Console.WriteLine("a");
    yield return 10;
    Console.WriteLine("b");
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

em (espécie de pseudocódigo):

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    __current = 10;
    return true;

case just_after_yield_return:
    try
    {
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        CatchBlock();
    }
    goto case post;

case post;
    Console.WriteLine("Post");


void CatchBlock()
{
    Console.WriteLine("Catch block");
}

A única duplicação está na configuração de blocos try / catch - mas isso é algo que o compilador certamente pode fazer.

Posso muito bem ter perdido algo aqui - se sim, por favor, me avise!

Jon Skeet
fonte
11
Uma boa prova de conceito, mas essa estratégia se torna dolorosa (provavelmente mais para um programador C # do que para um criador de compilador C #) assim que você começa a criar escopos com coisas como usinge foreach. Por exemplo:try{foreach (string s in c){yield return s;}}catch(Exception){}
Brian,
A semântica normal de "try / catch" implica que se qualquer parte de um bloco try / catch for ignorada devido a uma exceção, o controle será transferido para um bloco "catch" adequado, se houver. Infelizmente, se ocorrer uma exceção "durante" um retorno de rendimento, não há como o iterador distinguir os casos em que está sendo descartado por causa de uma exceção daqueles em que está sendo descartado porque o proprietário recuperou todos os dados de interesse.
supercat
7
"Suspeito que haja muito, muito poucas vezes em que essa restrição é realmente um problema que não pode ser contornado" Isso é como dizer que você não precisa de exceções porque você pode usar a estratégia de retorno de código de erro comumente usada em C, há muitos anos. Admito que as dificuldades técnicas podem ser significativas, mas isso ainda limita severamente a utilidade do yield, na minha opinião, por causa do código espaguete que você tem que escrever para contorná-lo.
jpmc26
@ jpmc26: Não, realmente não é como dizer isso. Não consigo me lembrar que isso alguma vez tenha me incomodado e usei blocos de iteradores várias vezes. Isso limita ligeiramente a utilidade do yieldIMO - está longe de ser severo .
Jon Skeet
2
Este 'recurso' realmente requer algum código bastante feio em alguns casos para solução alternativa, consulte stackoverflow.com/questions/5067188/…
namey
5

Todas as yieldinstruções em uma definição de iterador são convertidas em um estado em uma máquina de estado que usa efetivamente uma switchinstrução para avançar estados. Se ele fez gerar o código para yieldinstruções em um try / catch que teria de duplicar tudo no trybloco para cada yield declaração, excluindo todas as outras yielddeclaração para esse bloco. Isso nem sempre é possível, principalmente se uma yieldinstrução depender de outra anterior.

Mark Cidade
fonte
2
Eu não acho que acredito nisso. Acho que seria totalmente viável - mas muito complicado.
Jon Skeet
2
Os blocos try / catch em C # não foram feitos para serem reentrantes. Se você dividi-los, será possível chamar MoveNext () após uma exceção e continuar o bloco try com um estado possivelmente inválido.
Mark Cidade
2

Eu especularia que, devido à maneira como a pilha de chamadas é enrolada / desenrolada quando você produz o retorno de um enumerador, torna-se impossível para um bloco try / catch realmente "capturar" a exceção. (porque o bloco de retorno de rendimento não está na pilha, embora ele tenha originado o bloco de iteração)

Para obter uma ideia do que estou falando, configure um bloco iterador e um foreach usando esse iterador. Verifique a aparência da pilha de chamadas dentro do bloco foreach e, em seguida, verifique-a dentro do bloco iterador try / finally.

Radu094
fonte
Estou familiarizado com o desenrolamento de pilha em C ++, onde destruidores são chamados em objetos locais que saem do escopo. A coisa correspondente em C # seria try / finally. Mas esse retrocesso não ocorre quando ocorre o retorno do rendimento. E para try / catch, não há necessidade de interagir com o retorno do rendimento.
Daniel Earwicker
Verifique o que acontece com a pilha de chamadas durante o loop em um iterador e você entenderá o que quero dizer
Radu094
@ Radu094: Não, tenho certeza de que seria possível. Não se esqueça de que ele já controla finalmente, o que é pelo menos um pouco semelhante.
Jon Skeet
2

Aceitei a resposta de THE INVINCIBLE SKEET até que alguém da Microsoft apareceu para colocar água fria na ideia. Mas não concordo com a parte da questão de opinião - é claro que um compilador correto é mais importante do que um completo, mas o compilador C # já é muito inteligente em resolver essa transformação para nós na medida do possível. Um pouco mais de completude neste caso tornaria a linguagem mais fácil de usar, ensinar, explicar, com menos casos extremos ou pegadinhas. Então, acho que valeria a pena o esforço extra. Alguns caras em Redmond coçam a cabeça por quinze dias e, como resultado, milhões de programadores na próxima década podem relaxar um pouco mais.

(Eu também nutro um desejo sórdido de que haja uma maneira de yield returnlançar uma exceção que foi inserida na máquina de estado "de fora", pelo código que conduz a iteração. Mas minhas razões para querer isso são bastante obscuras.)

Na verdade, uma dúvida que tenho sobre a resposta de Jon tem a ver com o lançamento da expressão de retorno de rendimento.

Obviamente, o retorno de rendimento 10 não é tão ruim. Mas isso seria ruim:

yield return File.ReadAllText("c:\\missing.txt").Length;

Portanto, não faria mais sentido avaliar isso dentro do bloco try / catch anterior:

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
        __current = File.ReadAllText("c:\\missing.txt").Length;
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    return true;

O próximo problema seria blocos try / catch aninhados e exceções relançadas:

try
{
    Console.WriteLine("x");

    try
    {
        Console.WriteLine("a");
        yield return 10;
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        Console.WriteLine("y");

        if ((DateTime.Now.Second % 2) == 0)
            throw;
    }
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

Mas tenho certeza que é possível ...

Daniel Earwicker
fonte
1
Sim, você colocaria a avaliação em try / catch. Realmente não importa onde você coloca a configuração da variável. O ponto principal é que você pode efetivamente quebrar um único try / catch com retorno de rendimento em dois try / catch com retorno de rendimento entre eles.
Jon Skeet