Pourquoi une instruction «continue» ne peut-elle pas être dans un bloc «finalement»?

107

Je n'ai pas de problème; Je suis juste curieux. Imaginez le scénario suivant:

foreach (var foo in list)
{
    try
    {
         //Some code
    }
    catch (Exception)
    {
        //Some more code
    }
    finally
    {
        continue;
    }
}

Cela ne compilera pas, car cela déclenche l' erreur du compilateur CS0157 :

Le contrôle ne peut pas quitter le corps d'une clause finally

Pourquoi?

lpaloub
la source
7
Alors. Juste curieux. Si cela vous semble tout à fait logique de ne pas compiler, pourquoi voulez-vous que quelqu'un vous explique ce qui a déjà du sens? =)
J.Steen
14
pourquoi auriez - vous besoin d' un continue;en finallybloc? n'est-ce pas la même chose continue;qu'après le try -- catchbloc?
bansi
6
@ J.Steen Non, je sais que finally/continuec'est une limitation du compilateur C # :-) Je suis également curieux de connaître la raison de cette limitation.
xanatos
5
@xanatos - Techniquement parlant, c'est une limitation du CIL sous-jacent. D'après la spécification du langage : "Le transfert de contrôle n'est jamais autorisé à entrer un gestionnaire catch ou une clause finally sauf via le mécanisme de gestion des exceptions." et "Le transfert de contrôle hors d'une région protégée n'est autorisé que par le biais d'une instruction d'exception (Leave, end.filter, end.catch ou end.finally)." La brfamille d'instructions de branche ne peut pas accomplir cela.
Non signé
1
@Unsigned: c'est aussi une bonne limitation, ce qui serait bizarre :)
user7116

Réponses:

150

finallyles blocs s'exécutent, qu'une exception soit levée ou non. Si une exception est levée, qu'est-ce que diable feraitcontinue ? Vous ne pouvez pas continuer l'exécution de la boucle, car une exception non interceptée transférera le contrôle à une autre fonction.

Même si aucune exception n'est lancée, finally s'exécutera lorsque d'autres instructions de transfert de contrôle à l'intérieur du bloc try / catch s'exécutent, comme returnpar exemple, ce qui pose le même problème.

Bref, avec la sémantique de finally cela n'a pas de sens de permettre le transfert de contrôle de l'intérieur d'un finallybloc vers l'extérieur de celui-ci.

Soutenir cela avec une sémantique alternative serait plus déroutant qu'utile, car il existe des solutions de contournement simples qui clarifient le comportement prévu. Vous obtenez donc une erreur et êtes obligé de bien réfléchir à votre problème. C'est l'idée générale "vous jeter dans le gouffre du succès" qui se poursuit en C #.

C #, vous et le succès en cas de succès

Si vous voulez ignorer les exceptions (le plus souvent est une mauvaise idée) et continuer à exécuter la boucle, utilisez un bloc catch all:

foreach ( var in list )
{
    try{
        //some code
    }catch{
        continue;
    }
}

Si vous ne le souhaitez continueque lorsqu'aucune exception non interceptée n'est levée, mettez simplement en continuedehors du bloc try.

R. Martinho Fernandes
la source
12
J'accepterai cela comme réponse, car vous faites comprendre les raisons pour lesquelles Microsoft a éventuellement décidé de ne pas accepter une poursuite dans un enfin. Probablement que les images m'ont aussi convaincu :)
lpaloub
20
pouvez-vous élaborer l'idée «vous jeter dans le gouffre du succès»? Je ne l'ai pas compris :-D
Ant
13
L'image a été faite par Jon Skeet, btw. Je suis arrivé d'ici: msmvps.com/blogs/jon_skeet/archive/2010/09/02/…
R. Martinho Fernandes
1
Bien sûr, un continuedans un finallybloc sera OK s'il continue seulement une boucle locale définie à l'intérieur du finallybloc. Dans la question, cependant, il essaie de continuer une boucle «externe». Les déclarations similaires que vous ne pouvez pas avoir à l'intérieur d'un finallybloc sont return, break(lors de la sortie du bloc) et goto(lors du passage à une étiquette en dehors du finallybloc). Pour une discussion Java connexe, consultez Retour d'un bloc finally en Java .
Jeppe Stig Nielsen
32

Voici une source fiable:

Une instruction continue ne peut pas quitter un bloc finally (Section 8.10). Lorsqu'une instruction continue se produit dans un bloc finally, la cible de l'instruction continue doit être dans le même bloc finally; sinon, une erreur de compilation se produit.

Il est tiré de MSDN, 8.9.2 L'instruction continue .

La documentation dit que:

Les instructions d'un bloc finally sont toujours exécutées lorsque le contrôle quitte une instruction try. Cela est vrai que le transfert de contrôle se produise à la suite d'une exécution normale, suite à l'exécution d'une instruction break, continue, goto ou return, ou suite à la propagation d'une exception hors de l'instruction try. Si une exception est levée lors de l'exécution d'un bloc finally, l'exception est propagée à l'instruction try englobante suivante. Si une autre exception était en cours de propagation, cette exception est perdue. Le processus de propagation d'une exception est discuté plus loin dans la description de l'instruction throw (section 8.9.5).

C'est d'ici 8.10 L'instruction try .

Mortalus
la source
31

Vous pensez peut-être que cela a du sens, mais cela n'a pas de sens en fait.

foreach (var v in List)
{
    try
    {
        //Some code
    }
    catch (Exception)
    {
        //Some more code
        break; or return;
    }
    finally
    {
        continue;
    }
}

Qu'avez-vous l'intention de faire une pause ou une poursuite lorsqu'une exception est levée? L'équipe du compilateur C # ne veut pas prendre de décision par elle-même en supposantbreak ou continue. Au lieu de cela, ils ont décidé de se plaindre que la situation du développeur sera ambiguë pour transférer le contrôle finally block.

C'est donc le travail du développeur d'indiquer clairement ce qu'il a l'intention de faire plutôt que le compilateur en supposant autre chose.

J'espère que vous comprenez pourquoi cela ne compile pas!

Sriram Sakthivel
la source
Sur une note connexe, même s'il n'y avait pas de "catch", le chemin d'exécution suivant une finallyinstruction serait affecté par le fait que le catchsoit sorti via une exception, et il n'y a aucun mécanisme pour dire comment cela devrait interagir avec un fichier continue. J'aime votre exemple, car il montre un problème encore plus grave.
supercat
@supercat D'accord avec vous, ma réponse montre un exemple de situation ambiguë pour le compilateur mais il y a encore tellement de problèmes avec cette approche.
Sriram Sakthivel
16

Comme d'autres l'ont dit, mais en se concentrant sur les exceptions, il s'agit en réalité d'une gestion ambiguë du transfert de contrôle.

Dans votre esprit, vous pensez probablement à un scénario comme celui-ci:

public static object SafeMethod()
{
    foreach(var item in list)
    {
        try
        {
            try
            {
                //do something that won't transfer control outside
            }
            catch
            {
                //catch everything to not throw exceptions
            }
        }
        finally
        {
            if (someCondition)
                //no exception will be thrown, 
                //so theoretically this could work
                continue;
        }
    }

    return someValue;
}

Théoriquement, vous pouvez suivre le flux de contrôle et dire, oui, c'est "ok". Aucune exception n'est levée, aucun contrôle n'est transféré. Mais les concepteurs du langage C # avaient d'autres problèmes en tête.

L'exception lancée

public static void Exception()
{
    try
    {
        foreach(var item in list)
        {
            try
            {
                throw new Exception("What now?");
            }
            finally
            {
                continue;
            }
        }
    }
    catch
    {
        //do I get hit?
    }
}

Le redouté Goto

public static void Goto()
{
    foreach(var item in list)
    {
        try
        {
            goto pigsfly;
        }
        finally
        {
            continue;
        }
    }

    pigsfly:
}

Le retour

public static object ReturnSomething()
{
    foreach(var item in list)
    {
        try
        {
            return item;
        }
        finally
        {
            continue;
        }
    }
}

La rupture

public static void Break()
{
    foreach(var item in list)
    {
        try
        {
            break;
        }
        finally
        {
            continue;
        }
    }
}

Donc en conclusion, oui, bien qu'il y ait une légère possibilité d'utiliser un continuedans des situations où le contrôle n'est pas transféré, mais bon nombre (la majorité?) Des cas impliquent des exceptions ou des returnblocages. Les concepteurs de langage ont estimé que cela serait trop ambigu et (probablement) impossible à garantir au moment de la compilation que votre continuen'est utilisé que dans les cas où le flux de contrôle n'est pas transféré.

Chris Sinclair
la source
Vous avez la même situation en java lorsque proguard plantait lors de l'obfuscation. Votre explication est plutôt bonne. C # donne une erreur lors de la compilation - c'est parfaitement logique.
Dev_Vikram
11

En général, cela continuen'a pas de sens lorsqu'il est utilisé en finallybloc. Regarde ça:

foreach (var item in list)
{
    try
    {
        throw new Exception();
    }
    finally{
        //doesn't make sense as we are after exception
        continue;
    }
}
ghord
la source
«En général», beaucoup de choses n'ont pas de sens. Et cela ne veut pas dire que cela ne devrait pas fonctionner «en particulier». IE: continuen'a aucun sens en dehors de l'instruction de boucle, mais cela ne signifie pas qu'elle n'est pas prise en charge.
zerkms
Je pense que cela a du sens. itempeut être un fichier, la lecture a échoué -> finallyferme le fichier. continueempêche le reste du traitement.
jnovacho
5

"Cela ne se compilera pas et je pense que c'est tout à fait logique"

Eh bien, je pense que non.

Quand vous l'avez littéralement, catch(Exception)vous n'avez pas besoin de la dernière (et probablement même pas la continue).

Lorsque vous avez le plus réaliste catch(SomeException), que doit-il se passer lorsqu'une exception n'est pas interceptée? Vous continuevoulez aller dans un sens, l'exception en gérant une autre.

Henk Holterman
la source
2
Je pense que vous pourriez avoir besoin finallyquand vous en avez catch. Il est couramment utilisé pour fermer les ressources de manière fiable.
jnovacho
Ce ne sont pas que des exceptions. Vous pourriez facilement avoir une returndéclaration dans vos blocs tryou catch. Qu'est-ce qu'on fait maintenant? Revenir ou continuer la boucle? (et si nous continuons, que se passe-t-il si c'est le dernier élément à itérer? Ensuite, nous continuons juste en dehors de foreachet le retour ne se produit jamais?) EDIT: Ou même gotovers une étiquette en dehors de la boucle, à peu près toute action qui transfère le contrôle en dehors de la foreachboucle .
Chris Sinclair
2
Je ne suis pas d'accord avec "Quand vous avez littéralement catch (Exception) alors vous n'avez pas besoin du finally (et probablement même pas du continue).". Que faire si je dois effectuer une opération quelle que soit la levée ou non d'une exception?
lpaloub
OK, le enfin s'adresse à des returns supplémentaires à l'intérieur du bloc. Mais normalement, l'exécution se poursuit après le fourre-tout.
Henk Holterman
«enfin» n'est pas «attraper». Il doit être utilisé pour le code de nettoyage. un bloc finally s'exécute indépendamment du fait qu'une exception soit levée ou non . Des trucs comme la fermeture de fichiers, ou la libération de mémoire (si vous utilisez du code non managé) etc. ce sont les choses que vous mettez dans un bloc finally
Robotnik
3

Vous ne pouvez pas quitter le corps d'un bloc final. Cela inclut les mots clés break, return et dans votre cas continue.

Joseph Devlin
la source
3

Le finallybloc peut être exécuté avec une exception en attente d'être relancée. Cela n'aurait pas vraiment de sens de pouvoir sortir du bloc (par uncontinue ou quoi que ce soit d'autre) sans relancer l'exception.

Si vous voulez continuer votre boucle quoi qu'il arrive, vous n'avez pas besoin de l'instruction finally: attrapez simplement l'exception et ne la relancez pas.

Falanwe
la source
1

finallys'exécute qu'une exception non interceptée soit lancée ou non. D'autres ont déjà expliqué pourquoi cela rend continueillogique, mais voici une alternative qui suit l'esprit de ce que ce code semble demander. Fondamentalement, finally { continue; }c'est dire:

  1. Lorsqu'il y a des exceptions interceptées, continuez
  2. Lorsqu'il y a des exceptions non interceptées, autorisez-les à être lancées, mais continuez

(1) pourrait être satisfait en plaçant continueà la fin de chacun catch, et (2) pourrait être satisfait en stockant les exceptions non interceptées à lever plus tard. Vous pouvez l'écrire comme ceci:

var exceptions = new List<Exception>();
foreach (var foo in list) {
    try {
        // some code
    } catch (InvalidOperationException ex) {
        // handle specific exception
        continue;
    } catch (Exception ex) {
        exceptions.Add(ex);
        continue;
    }
    // some more code
}
if (exceptions.Any()) {
    throw new AggregateException(exceptions);
}

En fait, finallyaurait également exécuté dans le troisième cas, où il n'y avait aucune exception lancée, pris ou non. Si cela était souhaité, vous pourriez bien sûr en placer un continueaprès le bloc try-catch au lieu de l'intérieur de chacun catch.

nmclean
la source
1

Techniquement parlant, c'est une limitation du CIL sous-jacent. À partir de la spécification de langue :

Le transfert de contrôle n'est jamais autorisé à entrer un gestionnaire catch ou une clause finally, sauf via le mécanisme de gestion des exceptions.

et

Le transfert de contrôle hors d'une région protégée est autorisé uniquement par le biais d' une instruction d'exception ( leave, end.filter, end.catch, ou end.finally)

Sur la page doc pour les brinstructions :

Les transferts de contrôle vers et depuis les blocs try, catch, filter et finally ne peuvent pas être effectués par cette instruction.

Ce dernier est vrai pour toutes les instructions de branchement, y compris beq, brfalse, etc.

Non signé
la source
-1

Les concepteurs du langage ne voulaient tout simplement pas (ou ne pouvaient pas) raisonner sur la sémantique d'un bloc finally terminé par un transfert de contrôle.

Un problème, ou peut-être le problème clé, est que le finally bloc est exécuté dans le cadre d'un transfert de contrôle non local (traitement d'exception). La cible de ce transfert de contrôle n'est pas la boucle englobante; le traitement d'exception interrompt la boucle et continue le déroulement.

Si nous avons un transfert de contrôle hors du finally bloc de nettoyage, alors le transfert de contrôle d'origine est "détourné". Il est annulé et le contrôle va ailleurs.

La sémantique peut être élaborée. D'autres langues l'ont.

Les concepteurs de C # ont décidé de simplement interdire les transferts de contrôle statiques, «goto», simplifiant ainsi quelque peu les choses.

Cependant, même si vous faites cela, cela ne résout pas la question de savoir ce qui se passe si un transfert dynamique est lancé à partir d'un finally : que se passe-t-il si le bloc finally appelle une fonction et que cette fonction lève? Le traitement des exceptions d'origine est alors "détourné".

Si vous travaillez sur la sémantique de cette seconde forme de piratage, il n'y a aucune raison de bannir le premier type. C'est vraiment la même chose: un transfert de contrôle est un transfert de contrôle, qu'il ait la même portée lexicale ou non.

Kaz
la source
La différence est que, finallypar définition, il faut immédiatement suivre la poursuite du transfert qui l'a déclenché (une exception non interceptée return, etc.). Il serait facile d'autoriser les transferts "goto-like" à l'intérieur finally(quelles langues font cela, d'ailleurs?), Mais cela signifierait que cela try { return } finally { ... } pourrait ne pas revenir , ce qui est complètement inattendu. Si vous finallyappelez une fonction qui lance, ce n'est pas vraiment la même chose, car nous nous attendons déjà à ce que des exceptions se produisent à tout moment et interrompent le flux normal.
nmclean
@nmclean: En fait, une exception qui s'échappe finallyest en quelque sorte la même chose, en ce sens qu'elle peut entraîner un flux de programme inattendu et illogique. La seule différence est que les concepteurs de langage, étant incapables de rejeter tous les programmes où un bloc finally pourrait lever une exception non gérée, autorisent à la place ces programmes à se compiler et espèrent que le programme pourra accepter toutes les conséquences qui pourraient suivre l'abandon de l'exception précédente. séquence.
supercat
@supercat Je suis d'accord pour dire que cela rompt le flux attendu, mais mon point est que c'est toujours le cas pour les exceptions non gérées, que le flux actuel soit finallyou non. Selon la logique du dernier paragraphe de Kaz, les fonctions devraient également être libres d'affecter le flux dans la portée de leur appelant par le biais de continueetc., car elles sont autorisées à le faire à titre d'exceptions. Mais cela n'est bien sûr pas autorisé; les exceptions sont la seule exception à cette règle, d'où le nom «exception» (ou «interruption»).
nmclean
@nmclean: Les exceptions non imbriquées représentent un flux de contrôle qui diffère de l'exécution normale, mais qui est toujours structuré. L'instruction suivant un bloc ne doit s'exécuter que si toutes les exceptions qui se sont produites dans ce bloc y ont été capturées. Cela peut sembler une attente raisonnable, mais une exception qui se produit dans un finallybloc peut la violer. Vraiment une situation désagréable, qui à mon humble avis aurait dû être résolue en informant au minimum les finallyblocs de toutes les exceptions en attente afin qu'ils puissent créer des objets d'exception composites.
supercat
@mmclean Désolé, je ne suis pas d'accord que le nom "exception" provienne d'une "exception à la règle" (concernant le contrôle) spécifique à C #, LOL. L'aspect de modification du flux de contrôle d'une exception est simplement un transfert de contrôle dynamique non local. Un transfert de contrôle régulier dans le même périmètre lexical (aucun déroulement n'a lieu) peut être considéré comme un cas particulier de cela.
Kaz