La partie «enfin» d'une construction «essayer… attraper… enfin» est-elle même nécessaire?

25

Certains langages (tels que C ++ et les premières versions de PHP) ne prennent pas en charge la finallypartie d'une try ... catch ... finallyconstruction. Est-ce finallyjamais nécessaire? Parce que le code qu'il contient fonctionne toujours, pourquoi ne devrais-je / ne devrais-je pas simplement placer ce code après un try ... catchbloc sans finallyclause? Pourquoi en utiliser un? (Je cherche une raison / une motivation pour utiliser / ne pas utiliser finally, pas une raison pour supprimer la `` capture '' ou pourquoi il est légal de le faire.)

Agi Hammerthief
la source
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
maple_shaft

Réponses:

36

En plus de ce que d'autres ont dit, il est également possible qu'une exception soit levée à l'intérieur de la clause catch. Considère ceci:

try { 
    throw new SomeException();
} catch {
    DoSomethingWhichUnexpectedlyThrows();
}
Cleanup();

Dans cet exemple, la Cleanup()fonction ne s'exécute jamais, car une exception est levée dans la clause catch et la capture suivante la plus élevée dans la pile des appels la détectera. L'utilisation d'un bloc finally supprime ce risque et rend le code plus propre à démarrer.

Erik
la source
4
Merci pour une réponse concise et directe qui ne s'éloigne pas de la théorie et du territoire «la langue X vaut mieux que Y».
Agi Hammerthief
56

Comme d'autres l'ont mentionné, il n'y a aucune garantie que le code après une tryinstruction s'exécute à moins que vous n'attrapiez toutes les exceptions possibles. Cela dit, ceci:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   handleError();
} finally {
   cleanUp();
}

peut être réécrit 1 comme:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   try {
       handleError();
   } catch (Throwable e2) {
       cleanUp();
       throw e2;
   }
} catch (Throwable e) {
   cleanUp();
   throw e;
}
cleanUp();

Mais ce dernier vous oblige à intercepter toutes les exceptions non gérées, à dupliquer le code de nettoyage et à ne pas oublier de relancer. Ce finallyn'est donc pas nécessaire , mais c'est utile .

C ++ n'a pas finallyparce que Bjarne Stroustrup pense que RAII est meilleur , ou au moins suffit dans la plupart des cas:

Pourquoi C ++ ne fournit-il pas une construction "enfin"?

Parce que C ++ prend en charge une alternative presque toujours meilleure: la technique "l'acquisition de ressources est l'initialisation" (TC ++ PL3 section 14.4). L'idée de base est de représenter une ressource par un objet local, de sorte que le destructeur de l'objet local libère la ressource. De cette façon, le programmeur ne peut pas oublier de libérer la ressource.


1 Le code spécifique pour intercepter toutes les exceptions et relancer sans perdre les informations de trace de pile varie selon la langue. J'ai utilisé Java, où la trace de la pile est capturée lorsque l'exception est créée. En C #, vous utiliseriez simplement throw;.

Doval
la source
8
Vous devez également attraper les exceptions handleError()dans le deuxième cas, non?
Juri Robl
1
Vous pouvez également générer une erreur. Je reformulerais cela catch (Throwable t) {}, avec le bloc try .. catch autour du bloc initial entier (pour attraper aussi les objets jetables handleError)
njzk2
1
J'ajouterais en fait le try-catch supplémentaire que vous avez omis lors de l'appel, handleErro();ce qui en fera un argument encore meilleur pour expliquer pourquoi les blocs sont finalement utiles (même si ce n'était pas la question d'origine).
Alex
1
Cette réponse ne répond pas vraiment à la question de savoir pourquoi C ++ n'a pas finally, ce qui est beaucoup plus nuancé.
DeadMG
1
@AgiHammerthief L'imbriqué tryest à l'intérieur du catchpour l' exception spécifique . Deuxièmement, il est possible que vous ne sachiez pas si vous pouvez gérer l'erreur correctement jusqu'à ce que vous ayez examiné l'exception, ou que la cause de l'exception vous empêche également de gérer l'erreur (au moins à ce niveau). C'est assez courant lors des E / S. Le retour est là parce que la seule façon de garantir les cleanUpexécutions est de tout attraper , mais le code d'origine permettrait aux exceptions provenant du catch (SpecificException e)bloc de se propager vers le haut.
Doval
22

finally les blocs sont généralement utilisés pour vider les ressources, ce qui peut aider à la lisibilité lors de l'utilisation de plusieurs instructions de retour:

int DoSomething() {
    try {
        open_connection();
        return get_result();
    }
    catch {
        return 2;
    }
    finally {
        close_connection();
    }
}

contre

int DoSomething() {
    int result;
    try {
        open_connection();
        result = get_result();
    }
    catch {
        result = 2;
    }
    close_connection();
    return result;
}
AlexFoxGill
la source
2
Je pense que c'est la meilleure réponse. L'utilisation d'un finalement comme remplacement d'une exception générique semble juste merdique. Le cas d'utilisation correct consiste à nettoyer des ressources ou des opérations analogues.
Kik
3
Peut-être encore plus courant revient à l'intérieur du bloc try, plutôt qu'à l'intérieur du bloc catch.
Michael Anderson
À mon avis, le code n'explique pas adéquatement l'utilisation de finally. (J'utiliserais du code comme dans le deuxième bloc car les déclarations de retour multiples sont déconseillées là où je travaille.)
Agi Hammerthief
15

Comme vous l'avez apparemment déjà supposé, oui, C ++ fournit les mêmes capacités sans ce mécanisme. En tant que tel, à proprement parler, le mécanisme try/ finallyn'est pas vraiment nécessaire.

Cela dit, s'en passer impose des exigences sur la façon dont le reste du langage est conçu. En C ++, le même ensemble d'actions est incarné dans un destructeur de classe. Cela fonctionne principalement (exclusivement?) Car l'invocation de destructeurs en C ++ est déterministe. Ceci, à son tour, conduit à des règles assez complexes sur la durée de vie des objets, dont certaines sont décidément non intuitives.

La plupart des autres langues proposent à la place une forme de collecte des ordures. Bien qu'il y ait des choses sur la collecte des ordures qui sont controversées (par exemple, son efficacité par rapport à d'autres méthodes de gestion de la mémoire), une chose n'est généralement pas: l'heure exacte à laquelle un objet sera "nettoyé" par le garbage collector n'est pas directement liée à la portée de l'objet. Cela empêche son utilisation lorsque le nettoyage doit être déterministe, soit lorsqu'il est simplement requis pour un fonctionnement correct, soit lorsqu'il s'agit de ressources si précieuses que leur nettoyage ne doit pas être retardé arbitrairement. try/ finallyfournit un moyen pour ces langages de gérer les situations qui nécessitent ce nettoyage déterministe.

Je pense que ceux qui prétendent que la syntaxe C ++ pour cette capacité est "moins conviviale" que celle de Java manquent plutôt le point. Pire encore, il leur manque un point beaucoup plus crucial sur la division des responsabilités qui va bien au-delà de la syntaxe et qui a beaucoup plus à voir avec la façon dont le code est conçu.

En C ++, ce nettoyage déterministe se produit dans le destructeur de l'objet. Cela signifie que l'objet peut être (et devrait normalement être) conçu pour se nettoyer après lui-même. Cela rejoint l'essence de la conception orientée objet - une classe doit être conçue pour fournir une abstraction et appliquer ses propres invariants. En C ++, c'est exactement ce que l'on fait - et l'un des invariants qu'il prévoit est que lorsque l'objet est détruit, les ressources contrôlées par cet objet (toutes, pas seulement la mémoire) seront détruites correctement.

Java (et similaire) est quelque peu différent. Bien qu'ils prennent (en quelque sorte) en charge un finalizequi pourrait théoriquement fournir des capacités similaires, le support est si faible qu'il est fondamentalement inutilisable (et en fait, pratiquement jamais utilisé).

Par conséquent, plutôt que la classe elle - même puisse effectuer le nettoyage requis, le client de la classe doit prendre des mesures pour ce faire. Si nous faisons une comparaison suffisamment courte, il peut sembler à première vue que cette différence est assez mineure et Java est assez compétitif avec C ++ à cet égard. Nous nous retrouvons avec quelque chose comme ça. En C ++, la classe ressemble à ceci:

class Foo {
    // ...
public:
    void do_whatever() { if (xyz) throw something; }
    ~Foo() { /* handle cleanup */ }
};

... et le code client ressemble à ceci:

void f() { 
    Foo f;
    f.do_whatever();
    // possibly more code that might throw here
}

En Java, nous échangeons un peu plus de code où l'objet est utilisé pour un peu moins dans la classe. Cela ressemble initialement à un compromis assez uniforme. En réalité, c'est loin d'être le cas, car dans la plupart des codes, nous ne définissons la classe qu'à un seul endroit, mais nous l' utilisons à plusieurs endroits. L'approche C ++ signifie que nous écrivons uniquement ce code pour gérer le nettoyage en un seul endroit. L'approche Java signifie que nous devons écrire ce code pour gérer le nettoyage plusieurs fois, à de nombreux endroits - chaque endroit où nous utilisons un objet de cette classe.

En bref, l'approche Java garantit fondamentalement que de nombreuses abstractions que nous essayons de fournir sont "fuyantes" - toutes les classes qui nécessitent un nettoyage déterministe obligent le client de la classe à connaître les détails de ce qu'il faut nettoyer et comment faire le nettoyage , plutôt que ces détails étant cachés dans la classe elle-même.

Bien que je l'ai appelé "l'approche Java" ci-dessus, try/ finallyet des mécanismes similaires sous d'autres noms ne sont pas entièrement limités à Java. Pour un exemple frappant, la plupart (tous?) Des langages .NET (par exemple, C #) fournissent le même.

Les itérations récentes de Java et de C # fournissent également quelque chose à mi-chemin entre Java «classique» et C ++ à cet égard. En C #, un objet qui souhaite automatiser son nettoyage peut implémenter l' IDisposableinterface, qui fournit une Disposeméthode qui est (au moins vaguement) similaire à un destructeur C ++. Bien que cela puisse être utilisé via un try/ finallylike en Java, C # automatise un peu plus la tâche avec une usinginstruction qui vous permet de définir les ressources qui seront créées lors de la saisie d'une étendue, et détruites lorsque la portée sera fermée. Bien que toujours bien en deçà du niveau d'automatisation et de certitude fourni par C ++, il s'agit toujours d'une amélioration substantielle par rapport à Java. En particulier, le concepteur de classe peut centraliser les détails de la façon dontde disposer de la classe dans son implémentation IDisposable. Tout ce qui reste pour le programmeur client est le moindre fardeau d'écrire une usingdéclaration pour s'assurer que l' IDisposableinterface sera utilisée quand elle devrait l'être. Dans Java 7 et plus récent, les noms ont été modifiés pour protéger les coupables, mais l'idée de base est fondamentalement identique.

Jerry Coffin
la source
1
Réponse parfaite. Les destructeurs sont LA fonctionnalité incontournable en C ++.
Thomas Eding le
13

Je ne peux pas croire que personne d'autre n'ait soulevé cela (sans jeu de mots) - vous n'avez pas besoin d' une clause catch !

C'est parfaitement raisonnable:

try 
{
   AcquireManyResources(); 
   DoSomethingThatMightFail(); 
}
finally 
{
   CleanUpThoseResources(); 
}

Aucune clause catch nulle part n'est visible, car cette méthode ne peut rien faire d' utile avec ces exceptions; ils sont laissés pour propager sauvegarder la pile d'appels à un gestionnaire qui peut . Attraper et relancer des exceptions dans chaque méthode est une mauvaise idée, surtout si vous relancez simplement la même exception. Cela va complètement à l'encontre de la façon dont le traitement structuré des exceptions est censé fonctionner (et est assez proche du retour d'un "code d'erreur" de chaque méthode, juste sous la "forme" d'une exception).

Ce que cette méthode doit faire, cependant, pour se nettoyer après elle-même, afin que le "monde extérieur" n'ait jamais besoin de savoir quoi que ce soit dans le gâchis dans lequel il s'est engagé. La clause finally fait exactement cela - quel que soit le comportement des méthodes appelées, la clause finally sera exécutée "à la sortie" de la méthode (et il en va de même pour chaque clause finally entre le point auquel l'exception est levée et l'éventuelle clause catch qui le gère); chacun est exécuté comme la pile d'appels "se déroule".

Phill W.
la source
9

Que se passerait-il si une exception était levée à laquelle vous ne vous attendiez pas? L'essai se terminerait au milieu et aucune clause catch n'est exécutée.

Le bloc enfin est d'aider à cela et de s'assurer que, quelle que soit l'exception, le nettoyage se produira.

monstre à cliquet
la source
4
Ce n'est pas une raison suffisante pour un finally, car vous pouvez empêcher les exceptions "inattendues" avec catch(Object)ou catch(...)fourre-tout.
MSalters
1
Ce qui ressemble à un travail autour. Conceptuellement, c'est finalement plus propre. Mais je dois avouer que je l'utilise rarement.
quick_now
7

Certains langages proposent à la fois des constructeurs et des destructeurs pour leurs objets (par exemple C ++ je crois). Avec ces langages, vous pouvez faire la plupart (sans doute tous) de ce qui se fait habituellement finallydans un destructeur. En tant que telle - dans ces langues - une finallyclause peut être superflue.

Dans un langage sans destructeurs (par exemple Java), il est difficile (voire impossible) de réaliser un nettoyage correct sans la finallyclause. NB - En Java il y a une finaliseméthode mais il n'y a aucune garantie qu'elle sera jamais appelée.

OldCurmudgeon
la source
Il peut être utile de noter que les destructeurs aident à nettoyer les ressources lorsque la destruction est déterministe . Si nous ne savons pas quand l'objet sera détruit et / ou récupéré, les destructeurs ne sont pas suffisamment sûrs.
Morwenn
@Morwenn - Bon point. Je l'ai fait allusion à ma référence à Java, finalisemais je préférerais ne pas entrer dans les arguments politiques autour des destructeurs / finalités pour le moment.
OldCurmudgeon
En C ++, la destruction est déterministe. Lorsque la portée contenant un objet automatique se termine (par exemple, il est sorti de la pile), son destructeur est appelé. (C ++ vous permet d'allouer des objets sur la pile, pas seulement le tas.)
Rob K
@RobK - Et c'est la fonctionnalité exacte d'un finalisemais avec à la fois une saveur extensible et un mécanisme de type oop - très expressif et comparable au finalisemécanisme d'autres langages.
OldCurmudgeon
1

Essayez enfin et essayez d'attraper sont deux choses différentes qui ne partagent que le mot-clé: "essayer". Personnellement, j'aurais aimé voir ça différemment. La raison pour laquelle vous les voyez ensemble est que les exceptions produisent un "saut".

Et essayez enfin est conçu pour exécuter du code même si le flux de programmation saute. Que ce soit à cause d'une exception ou pour toute autre raison. C'est une bonne façon d'acquérir une ressource et de s'assurer qu'elle est nettoyée après sans avoir à se soucier des sauts.

Pieter B
la source
3
Dans .NET, ils sont implémentés à l'aide de mécanismes distincts; en Java, cependant, la seule construction reconnue par la JVM est sémantiquement équivalente à "on error goto", un modèle qui supporte directement try catchmais pas try finally; le code utilisant ce dernier est converti en code utilisant uniquement le premier, en copiant le contenu du finallybloc à tous les endroits du code où il peut être nécessaire de l'exécuter.
supercat
@supercat nice, merci pour les informations supplémentaires sur Java.
Pieter B
1

Étant donné que cette question ne spécifie pas C ++ comme langage, je considérerai un mélange de C ++ et Java, car ils adoptent une approche différente de la destruction d'objets, qui est suggérée comme l'une des alternatives.

Raisons pour lesquelles vous pourriez utiliser un bloc finally plutôt que du code après le bloc try-catch

  • vous revenez tôt du bloc try: considérez ceci

    Database db = null;
    try {
     db = open_database();
     if(db.isSomething()) {
       return 7;
     }
     return db.someThingElse();
    } finally {
      if(db!=null)
        db.close();
    }
    

    comparé à:

    Database db = null;
    int returnValue = 0;
    try {
     db = open_database();
     if(db.isSomething()) {
       returnValue = 7;
     } else {
       returnValue = db.someThingElse();
     }
    } catch(Exception e) {
      if(db!=null)
        db.close();
    }
    return returnValue;
    
  • vous revenez tôt du (des) bloc (s) de capture: comparer

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      return 7;
    } catch (DBIsADonkeyException e ) {
      return 11;
    } finally {
      if(db!=null)
        db.close();
    }
    

    contre:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null) 
        db.close();
      return 7;
    } catch (DBIsADonkeyException e ) {
      if(db!=null)
        db.close();
      return 11;
    }           
    db.close();
    
  • Vous renvoyez les exceptions. Comparer:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      throw convertToRuntimeException(e,"DB was wonkey");
    } finally {
      if(db!=null)
        db.close();
    }
    

    contre:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null)
        db.close();
      throw convertToRuntimeException(e,"DB was wonkey");
    } 
    if(db!=null)
      db.close();
    

Ces exemples ne semblent pas trop mauvais, mais souvent vous avez plusieurs de ces cas en interaction et plus d'un type d'exception / ressource en jeu. finallypeut aider à empêcher votre code de devenir un cauchemar de maintenance enchevêtré.

Maintenant, en C ++, ceux-ci peuvent être gérés avec des objets basés sur la portée. Mais l'OMI présente deux inconvénients: 1. la syntaxe est moins conviviale. 2. L'ordre de construction étant l'inverse de l'ordre de destruction peut rendre les choses moins claires.

En Java, vous ne pouvez pas accrocher la méthode finalize pour effectuer votre nettoyage, car vous ne savez pas quand cela se produira - (vous pouvez le faire, mais c'est un chemin rempli de conditions de course amusantes - JVM a beaucoup de latitude pour décider quand elle détruit les choses - souvent ce n'est pas quand vous vous y attendez - soit plus tôt ou plus tard que vous ne le pensez - et cela peut changer lorsque le compilateur de points chauds entre en jeu ... soupir ...)

Michael Anderson
la source
1

Tout ce qui est logiquement "nécessaire" dans un langage de programmation sont les instructions:

assignment a = b
subtract a from b
goto label
test a = 0
if true goto label

Tout algorithme peut être implémenté en utilisant uniquement les instructions ci-dessus, toutes les autres constructions de langage sont là pour rendre les programmes plus faciles à écrire et plus compréhensibles pour les autres programmeurs.

Voir oldy worldy computer pour le matériel réel en utilisant un ensemble d'instructions aussi minimal.

James Anderson
la source
1
Votre réponse est certainement vraie, mais je ne code pas en assembleur; c'est trop douloureux. Je me demande pourquoi utiliser une fonctionnalité dont je ne vois pas l'intérêt dans les langues qui la prennent en charge, pas quel est le jeu d'instructions minimal d'une langue.
Agi Hammerthief
1
Le fait est que n'importe quel langage implémentant uniquement ces 5 opérations peut implémenter n'importe quel algorithme - quoique plutôt tortueusement. La plupart des vers / opérateurs dans les langages de haut niveau ne sont pas "nécessaires" si le but est simplement de mettre en œuvre un algorithme. Si l'objectif est d'avoir un développement rapide de code lisible et maintenable, la plupart sont nécessaires mais "lisibles" et "maintenables" ne sont pas mesurables et extrêmement subjectifs. Les développeurs de langage sympa ont mis en place de nombreuses fonctionnalités: si vous n'avez pas d'utilisation pour certains d'entre eux, alors ne les utilisez pas.
James Anderson
0

En fait, le plus grand écart pour moi est généralement dans les langages qui prennent en charge finallymais manquent de destructeurs, car vous pouvez modéliser toute la logique associée au "nettoyage" (que je séparerai en deux catégories) via des destructeurs au niveau central sans traiter manuellement le nettoyage logique dans chaque fonction pertinente. Lorsque je vois du code C # ou Java faire des choses comme déverrouiller manuellement des mutex et fermer des fichiers en finallyblocs, cela semble obsolète et un peu comme le code C lorsque tout cela est automatisé en C ++ via des destructeurs de manière à libérer les humains de cette responsabilité.

Cependant, je trouverais toujours une commodité légère si C ++ était inclus finallyet c'est parce qu'il y a deux types de nettoyages:

  1. Détruire / libérer / déverrouiller / fermer / etc les ressources locales (les destructeurs sont parfaits pour cela).
  2. Annulation / annulation des effets secondaires externes (les destructeurs sont adéquats pour cela).

Le deuxième au moins ne correspond pas de manière aussi intuitive à l'idée de destruction des ressources, bien que vous puissiez le faire très bien avec les gardes de portée qui annulent automatiquement les modifications lorsqu'elles sont détruites avant d'être validées. Il finallyfournit sans doute au moins un mécanisme légèrement plus simple (juste un tout petit peu) pour le travail que les protecteurs de lunette.

Cependant, un mécanisme encore plus simple serait un rollbackbloc que je n'ai jamais vu dans aucune langue auparavant. C'est un peu mon rêve de pipe si j'ai jamais conçu un langage qui impliquait la gestion des exceptions. Cela ressemblerait à ceci:

try
{
    // Cause external side effects. These side effects should
    // be undone if we don't finish successfully.
}
rollback
{
    // Reverse external side effects. This block is *only* executed 
    // if the 'try' block above faced a premature return out 
    // of the function. It is different from 'finally' which 
    // gets executed regardless of whether or not the function 
    // exited prematurely. This block *only* gets executed if we 
    // exited prematurely from  the try block so that we can undo 
    // whatever side effects it failed to finish making. If the try 
    // block succeeded and didn't face a premature exit, then we 
    // don't want this block to execute.
}

Ce serait le moyen le plus simple de modéliser les restaurations d'effets secondaires, tandis que les destructeurs sont à peu près le mécanisme parfait pour le nettoyage des ressources locales. Maintenant, cela n'économise que quelques lignes de code supplémentaires de la solution de protection de portée, mais la raison pour laquelle je souhaite voir un langage avec cela est que la restauration des effets secondaires a tendance à être l'aspect le plus négligé (mais le plus délicat) de la gestion des exceptions. dans les langages qui tournent autour de la mutabilité. Je pense que cette fonctionnalité encouragerait les développeurs à réfléchir à la gestion des exceptions de manière appropriée en termes de restauration des transactions chaque fois que les fonctions provoquent des effets secondaires et ne se terminent pas et, en prime, lorsque les gens voient à quel point il peut être difficile de faire correctement des restaurations, ils pourraient préférer écrire plus de fonctions sans effets secondaires en premier lieu.

Il existe également des cas obscurs où vous souhaitez simplement effectuer diverses opérations, quelle que soit la sortie d'une fonction, quelle que soit la façon dont elle s'est terminée, comme l'enregistrement d'un horodatage, par exemple. Il finallyexiste sans doute la solution la plus simple et la plus parfaite pour le travail, car essayer d'instancier un objet uniquement pour utiliser son destructeur dans le seul but de journaliser un horodatage semble vraiment bizarre (même si vous pouvez le faire très bien et très facilement avec des lambdas ).


la source
-9

Comme tant d'autres choses inhabituelles sur le langage C ++, l'absence de try/finallyconstruction est un défaut de conception, si vous pouvez même l'appeler ainsi dans un langage qui semble souvent n'avoir eu aucun travail de conception réel .

RAII (l'utilisation de l'appel du destructeur déterministe basé sur la portée sur les objets basés sur la pile pour le nettoyage) a deux défauts graves. Le premier est qu'il nécessite l'utilisation d'objets basés sur la pile , qui sont une abomination qui violent le principe de substitution de Liskov. Il existe de nombreuses bonnes raisons pour lesquelles aucun autre langage OO avant ou depuis que C ++ ne les a utilisées - dans epsilon; D ne compte pas car il est fortement basé sur C ++ et n'a de toute façon aucune part de marché - et expliquer les problèmes qu'ils provoquent dépasse le cadre de cette réponse.

Deuxièmement, finally peut faire, c'est un surensemble de destruction d'objets. Une grande partie de ce qui est fait avec RAII en C ++ serait décrite dans le langage Delphi, qui n'a pas de récupération de place, avec le modèle suivant:

myObject := MyClass.Create(arguments);
try
   doSomething(myObject);
finally
   myObject.Free();
end;

C'est le modèle RAII rendu explicite; si vous deviez créer une routine C ++ qui ne contient que l'équivalent des première et troisième lignes ci-dessus, ce que le compilateur générerait finirait par ressembler à ce que j'ai écrit dans sa structure de base. Et parce que c'est le seul accès à latry/finally construction que C ++ fournit, les développeurs C ++ se retrouvent avec une vue plutôt myope try/finally: quand tout ce que vous avez est un marteau, tout commence à ressembler à un destructeur, pour ainsi dire.

Mais il y a d'autres choses qu'un développeur expérimenté peut faire avec un finally construction. Il ne s'agit pas de destruction déterministe, même face à une exception soulevée; il s'agit de l' exécution de code déterministe , même face à une exception levée.

Voici une autre chose que vous pouvez souvent voir dans le code Delphi: un objet de jeu de données auquel sont liés des contrôles utilisateur. L'ensemble de données contient des données provenant d'une source externe et les contrôles reflètent l'état des données. Si vous êtes sur le point de charger un tas de données dans votre ensemble de données, vous souhaiterez désactiver temporairement la liaison de données afin qu'il ne fasse pas des choses étranges à votre interface utilisateur, en essayant de la mettre à jour encore et encore avec chaque nouvel enregistrement entré , vous devez donc le coder comme ceci:

dataset.DisableControls();
try
   LoadData(dataset);
finally
   dataset.EnableControls();
end;

De toute évidence, aucun objet n'est détruit ici et aucun besoin. Le code est simple, concis, explicite et efficace.

Comment cela se ferait-il en C ++? Eh bien, vous devez d'abord coder une classe entière . Ce serait probablement appelé DatasetEnablerou quelque chose comme ça. Son existence entière serait celle d'un assistant RAII. Ensuite, vous devrez faire quelque chose comme ceci:

dataset.DisableControls();
{
   raiiGuard = DatasetEnabler(dataset);
   LoadData(dataset);
}

Oui, ces accolades apparemment superflues sont nécessaires pour gérer la portée appropriée et garantir que l'ensemble de données est réactivé immédiatement et non à la fin de la méthode. Donc, ce que vous obtenez ne prend pas moins de lignes de code (sauf si vous utilisez des accolades égyptiennes). Il nécessite la création d'un objet superflu, qui a des frais généraux. (Le code C ++ n'est-il pas censé être rapide?) Il n'est pas explicite, mais s'appuie à la place sur la magie du compilateur. Le code qui est exécuté n'est décrit nulle part dans cette méthode, mais réside à la place dans une classe entièrement différente, éventuellement dans un fichier entièrement différent . Bref, ce n’est en aucun cas une meilleure solution que d’écrire letry/finally bloc vous-même.

Ce type de problème est suffisamment courant dans la conception d'un langage pour qu'il y ait un nom: l'inversion d'abstraction. Cela se produit lorsqu'une construction de haut niveau est construite au-dessus d'une construction de bas niveau, puis que la construction de bas niveau n'est pas directement prise en charge dans le langage, obligeant ceux qui souhaitent l'utiliser à la réimplémenter en termes de construction de haut niveau, souvent avec des pénalités importantes pour la lisibilité et l'efficacité du code.

Mason Wheeler
la source
Les commentaires visent à clarifier ou à améliorer une question et une réponse. Si vous souhaitez avoir une discussion sur cette réponse, veuillez vous rendre dans la salle de chat. Merci.
maple_shaft