web-dev-qa-db-fra.com

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

Ce qui suit est correct:

try
{
    Console.WriteLine("Before");

    yield return 1;

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

Le bloc finally s'exécute lorsque tout est terminé (IEnumerator<T> prend en charge IDisposable 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 les besoins de l'argument) qu'une exception est levée par l'un ou l'autre des appels WriteLine à l'intérieur du bloc try. Quel est le problème avec la poursuite de l'exécution dans le bloc catch?

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

Mise à jour: Il y a un commentaire intéressant d'Eric Lippert ici - semble qu'ils ont 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.

89
Daniel Earwicker

Je soupçonne que c'est une question d'ordre pratique 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 résolu - mais la complexité supplémentaire dans le compilateur serait très importante.

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

  • Les attributs ne pouvant pas être génériques
  • Incapacité de X à dériver de X.Y (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érerais un langage légèrement plus restrictif avec un compilateur précis à 99,9% (oui, il y a des bugs; j'en ai rencontré un sur SO = juste l'autre jour) qu'une langue plus flexible qui ne pouvait pas compiler correctement.

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

Considérez que:

  • Vous pouvez vous assurer que la partie de retour de rendement elle-même ne lève pas d'exception (précalculez la valeur, puis vous définissez simplement un champ et retournez "true")
  • Vous êtes autorisé à essayer/attraper qui n'utilise pas le retour de rendement dans un bloc d'itérateur.
  • Toutes les variables locales du bloc 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

Transformez maintenant:

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

en (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 réside 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!

48
Jon Skeet

Toutes les instructions yield dans une définition d'itérateur sont converties en un état dans une machine à états qui utilise efficacement une instruction switch pour avancer les états. S'il faisait générer du code pour les instructions yield dans un try/catch, il devrait dupliquer tout dans le bloc try pour - chaqueyield instruction en excluant toutes les autres instructions yield pour ce bloc. Ce n'est pas toujours possible, en particulier si une instruction yield dépend d'une précédente.

5
Mark Cidade

Je suppose que, en raison de la façon dont la pile d'appels est enroulée/déroulée lorsque vous retournez d'un énumérateur, il devient impossible pour un bloc try/catch de "capturer" réellement 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 à l'aide de cet itérateur. Vérifiez à quoi ressemble la pile d'appels à l'intérieur du bloc foreach, puis vérifiez-la à l'intérieur du bloc try/finally de l'itérateur.

2
Radu094

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 question de l'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 la langue plus facile à utiliser, à enseigner, à expliquer, avec moins de cas Edge ou de pièges. Je pense donc que cela en vaudrait la peine. Quelques gars de Redmond se grattent la tête pendant quinze 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 également un désir sordide de trouver un moyen de faire yield return lève une exception qui a été insérée dans la machine d'état "de l'extérieur", par le code entraînant l'itération. Mais mes raisons de vouloir cela sont assez obscures.)

En fait, une question que j'ai sur la réponse de Jon concerne le lancement de l'expression de retour de rendement.

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

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

Il ne serait donc pas plus logique d'évaluer cela à l'intérieur du 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 prochain problème serait les blocs try/catch imbriqués et les exceptions renvoyé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 ...

2
Daniel Earwicker