L'utilisation de la clause finally pour effectuer un travail après le retour est-elle de mauvais style / dangereuse?


30

Dans le cadre de l'écriture d'un itérateur, je me suis retrouvé à écrire le code suivant (suppression de la gestion des erreurs)

public T next() {
  try {
    return next;
  } finally {
    next = fetcher.fetchNext(next);
  }
}

le trouvant un peu plus facile à lire que

public T next() {
  T tmp = next;
  next = fetcher.fetchNext(next);
  return tmp;
}

Je sais que c'est un exemple simple, où la différence de lisibilité n'est peut-être pas si écrasante, mais je suis intéressé par l'opinion générale quant à savoir s'il est mauvais d'utiliser try-finally dans des cas comme celui-ci où il n'y a pas d'exceptions impliquées, ou s'il est réellement préféré lorsqu'il simplifie le code.

Si c'est mauvais: pourquoi? Style, performance, pièges, ...?

Conclusion Merci toutes vos réponses! Je suppose que la conclusion (au moins pour moi) est que le premier exemple aurait pu être plus lisible s'il s'agissait d'un modèle courant, mais qu'il ne l'est pas. Par conséquent, la confusion introduite par l'utilisation d'une construction en dehors de son objectif, ainsi que le flux d'exceptions éventuellement obscurci, l'emporteront sur toutes les simplifications.


10
Personnellement, je pourrais comprendre le second plus que le premier, mais j'utilise rarement des finallyblocs.
Christian Mann

2
return current = fetcher.fetchNext (current); // Que diriez-vous de cela, aka avez-vous vraiment besoin de la pré-lecture?
scarfridge

1
@scarfridge L'exemple concerne l'implémentation Iterator, où vous avez en effet besoin d'une sorte de prélecture pour hasNext()fonctionner. Essayez-le vous-même.
maaartinus

1
@maaartinus J'en suis conscient. Parfois, hasNext () renvoie simplement true, par exemple la séquence de grêlons et parfois, la récupération de l'élément suivant est d'un coût prohibitif. Dans ce dernier cas, vous devez recourir à la récupération de l'élément uniquement à la demande, comme pour l'initialisation paresseuse.
scarfridge

1
@scarfridge Le problème avec l'utilisation courante de Iteratorest que vous devez récupérer la valeur dans hasNext()(car la chercher est souvent le seul moyen de savoir si elle existe) et la renvoyer next()comme l'OP l'a fait.
maaartinus

Réponses:


18

Personnellement, je réfléchirais à l'idée de la séparation des requêtes de commande . Lorsque vous y arrivez, next () a deux objectifs: l'objectif annoncé de récupérer l'élément next et l'effet secondaire caché de la mutation de l'état interne de next. Donc, ce que vous faites, c'est atteindre l'objectif annoncé dans le corps de la méthode, puis s'attaquer à un effet secondaire caché dans une clause finalement, ce qui semble ... maladroit, mais pas `` faux '', exactement.

Ce qui se résume vraiment à la façon dont le code est compréhensible, je dirais, et dans ce cas, la réponse est "meh". Vous "essayez" une simple déclaration de retour, puis vous exécutez un effet secondaire caché dans un bloc de code qui est censé être pour la récupération d'erreur à sécurité intégrée. Ce que vous faites est «intelligent», et le code «intelligent» incite souvent les mainteneurs à marmonner «ce que le… oh, je pense que je comprends. Il vaut mieux que les gens lisant votre code marmonnent "oui, euh-huh, c'est logique, oui ..."

Et si vous sépariez la mutation d'état des appels des accesseurs? J'imagine que le problème de lisibilité qui vous préoccupe devient théorique, mais je ne sais pas comment cela affecte votre abstraction plus large. Quelque chose à considérer, de toute façon.


1
+1 pour le commentaire "intelligent". Cela capture très précisément cet exemple.

2
L'action 'retour et avance' de next () est forcée sur l'OP par l'interface itérative Java lourde.
kevin cline

37

Purement du point de vue du style, je pense que ces trois lignes:

T current = next;
next = fetcher.getNext();
return current;

... sont à la fois plus évidents et plus courts qu'un bloc try / finally. Comme vous ne vous attendez pas à ce que des exceptions soient levées, l'utilisation d'un trybloc va juste confondre les gens.


2
Un bloc try / catch / finally peut également ajouter une surcharge supplémentaire, je ne sais pas si javac ou le compilateur JIT les supprimerait même si vous ne lançiez pas d'exception.
Martijn Verburg

2
@MartijnVerburg ouais, javac ajoute toujours une entrée à la table des exceptions et duplique le code finalement même si le bloc try est vide.
Daniel Lubarov

@Daniel - merci j'étais trop paresseux pour exécuter javap :-)
Martijn Verburg

@Martijn Verburg: AFAIK, un bloc try-catch-finallt est essentiellement gratuit tant qu'aucune exception réelle n'est levée. Ici, javac n'essaie pas et n'a pas besoin d'optimiser quoi que ce soit car JIT s'en occupera.
maaartinus

@maaartinus - merci qui a du sens - besoin de relire le code source d'OpenJDK :-)
Martijn Verburg

6

Cela dépendrait du but du code à l'intérieur du bloc finally. L'exemple canonique est la fermeture d'un flux après sa lecture / écriture, une sorte de "nettoyage" qui doit toujours être fait. La restauration d'un objet (dans ce cas, un itérateur) dans un état valide à mon humble avis compte également comme un nettoyage, donc je ne vois aucun problème ici. Si OTOH que vous utilisiez returndès que votre valeur de retour a été trouvée et que vous ajoutiez beaucoup de code non lié dans le bloc finally, cela obscurcirait le but et le rendrait moins compréhensible.

Je ne vois aucun problème à l'utiliser quand "il n'y a pas d'exceptions impliquées". Il est très courant de l'utiliser try...finallysans un catchlorsque le code ne peut que lancer RuntimeExceptionet que vous ne prévoyez pas de les gérer. Parfois, le n'est finalement qu'une sauvegarde, et vous savez pour la logique de votre programme qu'aucune exception ne sera jamais levée (la condition classique "cela ne devrait jamais arriver").

Pièges: toute exception levée à l'intérieur du trybloc fera finallycourir le bock. Cela peut vous mettre dans un état incohérent. Donc, si votre déclaration de retour était quelque chose comme:

return preProcess(next);

et ce code a soulevé une exception, fetchNextserait toujours exécuté. OTOH si vous l'avez codé comme:

T ret = preProcess(next);
next = fetcher.fetchNext(next);
return ret;

alors il ne fonctionnerait pas. Je sais que vous supposez que le code try ne peut jamais déclencher d'exception, mais pour les cas plus complexes que celui-ci, comment pouvez-vous en être sûr? Si votre itérateur était un objet à longue durée de vie, cela continuerait d'exister même si une erreur irrécupérable s'est produite dans le thread actuel, il serait important de le garder dans un état valide à tout moment. Sinon, peu importe vraiment ...

Performances: il serait intéressant de décompiler un tel code pour voir comment il fonctionne sous le capot, mais je ne connais pas assez la JVM pour même faire une bonne supposition ... Les exceptions sont généralement "exceptionnelles", donc le code qui gère ils n'ont pas besoin d'être optimisés pour la vitesse (d'où le conseil de ne jamais utiliser d'exceptions dans le flux de contrôle normal de vos programmes), mais je ne sais pas finally.


N'êtes-vous pas en fait en train de fournir une assez bonne raison pour éviter d' utiliser finallyde cette façon? Je veux dire, comme vous le dites, le code finit par avoir des conséquences indésirables; pire encore, vous ne pensez probablement même pas aux exceptions.
Andres F.

2
La vitesse d'écoulement de contrôle normal n'est pas significativement affectée par les constructions de gestion des exceptions. La pénalité de performance n'est payée que lors du lancement et de la capture d'exceptions, pas lors de la saisie d'un bloc d'essai ou de la saisie d'un bloc finalement en fonctionnement normal.
yfeldblum

1
@AndresF. Vrai, mais c'est également valable pour tout code de nettoyage. Par exemple, supposons qu'une référence à un flux ouvert soit définie nullpar un code buggy; essayer de lire à partir de lui lèvera une exception, essayer de le fermer dans le finallybloc lèvera une autre exception. En outre, il s'agit d'une situation où l'état du calcul n'a pas d'importance, vous souhaitez fermer le flux, qu'une exception se soit produite ou non. Il pourrait également y avoir d'autres situations de ce genre. Pour cette raison, je le traite comme un piège pour les utilisations valables, plutôt que comme une recommandation de ne jamais l'utiliser.
mgibsonbr

Il y a aussi le problème que lorsque le try et finalement le block lèvent une exception, l'exception dans le try n'est jamais vue par personne, mais c'est probablement la cause du problème.
Hans-Peter Störr

3

L'énoncé de titre: "... enfin clause pour effectuer le travail après le retour ..." est faux. Le bloc finalement se produit avant le retour de la fonction. C'est tout l'intérêt de finalement en fait.

Ce que vous abusez ici, c'est l'ordre d'évaluation, où la valeur de next est stockée pour être retournée avant de la muter. Ce n'est pas une pratique courante et à mon avis, c'est faux car cela vous rend le code non séquentiel et donc beaucoup plus difficile à suivre.

L'utilisation prévue des blocs finally est pour la gestion des exceptions où certaines conséquences doivent se produire indépendamment des exceptions levées, par exemple le nettoyage de certaines ressources, la fermeture de la connexion DB, la fermeture d'un fichier / socket, etc.


+1, j'ajouterais que même si la spécification de langue garantit que la valeur est stockée avant d'être renvoyée, elle va pitoyablement à l'encontre des attentes du lecteur et doit donc être évitée lorsque cela est raisonnablement possible. Le débogage est deux fois plus difficile que le codage ...
jmoreno

0

Je serais très prudent, car (en général, pas dans votre exemple), une partie dans try peut lever l'exception, et si elle est jetée, rien ne sera retourné et si vous avez réellement l'intention d'exécuter ce qui se trouve finalement après le retour, cela exécuter quand vous ne le vouliez pas.

Et un autre bidon de ver est, qu'en général, une action utile peut finalement lever des exceptions, donc vous pouvez terminer avec une méthode vraiment compliquée.

À cause de cela, quelqu'un qui le lit doit penser à ces problèmes, donc c'est plus difficile à comprendre.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.