Pourquoi le rendement de rendement ne peut-il pas apparaître dans un bloc try avec un catch?

95

Ce qui suit est correct:

try
{
    Console.WriteLine("Before");

    yield return 1;

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

Le finallybloc s'exécute lorsque le tout a fini de s'exécuter ( IEnumerator<T>prend IDisposableen charge pour fournir un moyen de garantir cela même lorsque l'énumération est abandonnée avant la fin).

Mais ce n'est pas correct:

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);
}

Supposons (pour des raisons d'argumentation) qu'une exception soit levée par l'un ou l'autre des WriteLineappels à l'intérieur du bloc try. Quel est le problème avec la poursuite de l'exécution en catchbloc?

Bien sûr, la partie yield return est (actuellement) incapable de lancer quoi que ce soit, mais pourquoi cela devrait-il nous empêcher d'avoir une clôture try/ catchtraiter les exceptions levées avant ou après un yield return?

Mise à jour: Il y a un commentaire intéressant d'Eric Lippert ici - il semble qu'ils aient déjà suffisamment de problèmes pour implémenter correctement le comportement try / finally!

EDIT: La page MSDN sur cette erreur est: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx . Cela n'explique cependant pas pourquoi.

Daniel Earwicker
la source
2
Lien direct vers le commentaire d'Eric Lippert: blogs.msdn.com/oldnewthing/archive/2008/08/14/…
Roman Starkov
note: vous ne pouvez pas non plus céder le bloc catch lui-même :-(
Simon_Weaver
2
Le lien oldnewthing ne fonctionne plus.
Sebastian Redl

Réponses:

50

Je soupçonne que c'est une question de praticité plutôt que de faisabilité. Je soupçonne qu'il y a très, très peu de fois où cette restriction est en fait un problème qui ne peut pas être contourné - mais la complexité supplémentaire dans le compilateur serait très importante.

Il y a quelques choses comme celle-ci que j'ai déjà rencontrées:

  • Les attributs ne pouvant pas être génériques
  • Impossibilité pour X de dériver de XY (une classe imbriquée dans X)
  • Blocs d'itérateur utilisant des champs publics dans les classes générées

Dans chacun de ces cas, il serait possible de gagner un peu plus de liberté, au prix d'une complexité supplémentaire dans le compilateur. L'équipe a fait le choix pragmatique, pour lequel je les applaudis - je préfère avoir un langage un peu plus restrictif avec un compilateur précis à 99,9% (oui, il y a des bugs; j'en ai rencontré un sur SO l'autre jour) qu'un plus langage flexible qui n'a pas pu se compiler correctement.

EDIT: Voici une pseudo-preuve de la raison pour laquelle c'est faisable.

Considérez cela:

  • Vous pouvez vous assurer que la partie yield return elle-même ne lève pas d'exception (précalculez la valeur, puis vous définissez simplement un champ et renvoyez "true")
  • Vous êtes autorisé à essayer / attraper qui n'utilise pas yield return dans un bloc d'itérateur.
  • Toutes les variables locales du bloc d'itérateur sont des variables d'instance dans le type généré, vous pouvez donc déplacer librement le code vers de nouvelles méthodes

Maintenant, transformez:

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

into (sorte de pseudo-code):

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");
}

La seule duplication est dans la configuration des blocs try / catch - mais c'est quelque chose que le compilateur peut certainement faire.

J'ai peut-être manqué quelque chose ici - si oui, faites-le moi savoir!

Jon Skeet
la source
11
Une bonne preuve de concept, mais cette stratégie devient pénible (probablement plus pour un programmeur C # que pour un rédacteur de compilateur C #) une fois que vous commencez à créer des étendues avec des choses comme usinget foreach. Par exemple:try{foreach (string s in c){yield return s;}}catch(Exception){}
Brian
La sémantique normale de "try / catch" implique que si une partie d'un bloc try / catch est ignorée à cause d'une exception, le contrôle sera transféré vers un bloc "catch" approprié s'il en existe un. Malheureusement, si une exception se produit "pendant" un retour de rendement, l'itérateur n'a aucun moyen de distinguer les cas où il est supprimé en raison d'une exception de ceux où il est supprimé, car le propriétaire a récupéré toutes les données d'intérêt.
supercat du
7
"Je soupçonne qu'il y a très, très peu de fois où cette restriction est en fait un problème qui ne peut pas être contourné" C'est un peu comme dire que vous n'avez pas besoin d'exceptions parce que vous pouvez utiliser la stratégie de retour de code d'erreur couramment utilisée en C. il y a de nombreuses années. J'admets que les difficultés techniques peuvent être importantes, mais cela limite encore considérablement l'utilité de yield, à mon avis, à cause du code spaghetti que vous devez écrire pour le contourner.
jpmc26
@ jpmc26: Non, ce n'est vraiment pas comme dire ça du tout. Je ne me souviens pas que cela m'ait jamais mordu, et j'ai utilisé des blocs d'itération plusieurs fois. Cela limite légèrement l'utilité de l' yieldOMI - c'est loin d'être sévère .
Jon Skeet
2
Cette `` fonctionnalité '' nécessite en fait un code plutôt laid dans certains cas pour contourner le
problème
5

Toutes les yieldinstructions d'une définition d'itérateur sont converties en un état dans une machine à états qui utilise effectivement une switchinstruction pour avancer les états. Si elle a fait générer du code pour les yielddéclarations dans un try / catch il faudrait dupliquer tout dans le trybloc pour chaque yield déclaration tout en excluant toute autre yielddéclaration pour ce bloc. Ce n'est pas toujours possible, en particulier si une yieldinstruction dépend d'une précédente.

Mark Cidade
la source
2
Je ne pense pas acheter ça. Je pense que ce serait tout à fait faisable - mais très compliqué.
Jon Skeet
2
Les blocs Try / catch en C # ne sont pas destinés à être rentrants. Si vous les divisez, il est possible d'appeler MoveNext () après une exception et de continuer le bloc try avec un état éventuellement invalide.
Mark Cidade
2

Je suppose qu'en raison de la façon dont la pile d'appels est enroulée / déroulée lorsque vous donnez un retour d'un énumérateur, il devient impossible pour un bloc try / catch de réellement "capturer" l'exception. (parce que le bloc de retour de rendement n'est pas sur la pile, même s'il est à l'origine du bloc d'itération)

Pour avoir une idée de ce dont je parle, configurez un bloc d'itérateur et un foreach en utilisant cet itérateur. Vérifiez à quoi ressemble la pile d'appels à l'intérieur du bloc foreach, puis vérifiez-la dans le bloc try / finally de l'itérateur.

Radu094
la source
Je suis familier avec le déroulement de pile en C ++, où les destructeurs sont appelés sur des objets locaux qui sortent de la portée. La chose correspondante en C # serait try / finally. Mais ce déroulement ne se produit pas lorsque le rendement du rendement se produit. Et pour try / catch, il n'est pas nécessaire qu'il interagisse avec le rendement du rendement.
Daniel Earwicker
Vérifiez ce qui arrive à la pile d'appels lors de la boucle sur un itérateur et vous comprendrez ce que je veux dire
Radu094
@ Radu094: Non, je suis sûr que ce serait possible. N'oubliez pas qu'il gère déjà enfin, ce qui est au moins un peu similaire.
Jon Skeet
2

J'ai accepté la réponse de THE INVINCIBLE SKEET jusqu'à ce que quelqu'un de Microsoft vienne verser de l'eau froide sur l'idée. Mais je ne suis pas d'accord avec la partie question d'opinion - bien sûr, un compilateur correct est plus important qu'un compilateur complet, mais le compilateur C # est déjà très intelligent pour trier cette transformation pour nous dans la mesure où il le fait. Un peu plus d'exhaustivité dans ce cas rendrait le langage plus facile à utiliser, à enseigner, à expliquer, avec moins de cas extrêmes ou de pièges. Je pense donc que cela en vaudrait la peine. Quelques gars à Redmond se grattent la tête pendant une quinzaine de jours et, par conséquent, des millions de codeurs au cours de la prochaine décennie peuvent se détendre un peu plus.

(Je nourris aussi un désir sordide qu'il y ait un moyen de faire yield returnjeter une exception qui a été insérée dans la machine à états "de l'extérieur", par le code conduisant l'itération. Mais mes raisons de vouloir cela sont assez obscures.)

En fait, une question que j'ai à propos de la réponse de Jon est liée au lancement de l'expression de retour de rendement.

De toute évidence, le rendement de rendement 10 n'est pas si mal. Mais ce serait mauvais:

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

Il ne serait donc pas plus logique d'évaluer cela dans le bloc try / catch précédent:

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;

Le problème suivant serait les blocs try / catch imbriqués et les exceptions relancées:

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");

Mais je suis sûr que c'est possible ...

Daniel Earwicker
la source
1
Oui, vous mettriez l'évaluation dans le try / catch. Peu importe où vous mettez le paramètre de variable. Le point principal est que vous pouvez effectivement diviser un seul essai / capture avec un rendement de rendement en deux essais / prises avec un rendement de rendement entre eux.
Jon Skeet