Je suis surpris que personne n'ait suggéré cette alternative, donc même si la question existe depuis un certain temps, je vais l'ajouter: une bonne façon de résoudre ce problème est d'utiliser des variables pour garder une trace de l'état actuel. Il s'agit d'une technique qui peut être utilisée, qu'elle soit ou non goto
utilisée pour arriver au code de nettoyage. Comme toute technique de codage, elle a des avantages et des inconvénients et ne conviendra pas à toutes les situations, mais si vous choisissez un style, cela vaut la peine d'être pris en compte - surtout si vous voulez éviter goto
sans vous retrouver avec des if
s profondément imbriqués .
L'idée de base est que, pour chaque action de nettoyage qui pourrait être nécessaire, il existe une variable à partir de laquelle nous pouvons dire si le nettoyage doit être effectué ou non.
Je vais d'abord montrer la goto
version, car elle est plus proche du code de la question d'origine.
int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
/*
* Prepare
*/
if (do_something(bar)) {
something_done = 1;
} else {
goto cleanup;
}
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
goto cleanup;
}
if (prepare_stuff(bar)) {
stufF_prepared = 1;
} else {
goto cleanup;
}
/*
* Do the thing
*/
return_value = do_the_thing(bar);
/*
* Clean up
*/
cleanup:
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
Un avantage de ceci par rapport à certaines des autres techniques est que, si l'ordre des fonctions d'initialisation est changé, le nettoyage correct se produira toujours - par exemple, en utilisant la switch
méthode décrite dans une autre réponse, si l'ordre d'initialisation change, alors le switch
doit être édité très soigneusement pour éviter d'essayer de nettoyer quelque chose qui n'a pas été réellement initialisé en premier lieu.
Maintenant, certains pourraient argumenter que cette méthode ajoute un grand nombre de variables supplémentaires - et en effet dans ce cas c'est vrai - mais en pratique, une variable existante suit déjà, ou peut être amenée à suivre, l'état requis. Par exemple, si le prepare_stuff()
est en fait un appel à malloc()
, ou à open()
, alors la variable contenant le pointeur ou le descripteur de fichier renvoyé peut être utilisée - par exemple:
int fd = -1;
....
fd = open(...);
if (fd == -1) {
goto cleanup;
}
...
cleanup:
if (fd != -1) {
close(fd);
}
Maintenant, si nous suivons en plus l'état de l'erreur avec une variable, nous pouvons éviter goto
complètement, et toujours nettoyer correctement, sans avoir d'indentation qui devient de plus en plus profonde au fur et à mesure que nous avons besoin d'initialisation:
int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
int oksofar = 1;
/*
* Prepare
*/
if (oksofar) { /* NB This "if" statement is optional (it always executes) but included for consistency */
if (do_something(bar)) {
something_done = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (prepare_stuff(bar)) {
stuff_prepared = 1;
} else {
oksofar = 0;
}
}
/*
* Do the thing
*/
if (oksofar) {
return_value = do_the_thing(bar);
}
/*
* Clean up
*/
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
Encore une fois, il y a des critiques potentielles à ce sujet:
- Tous ces «si» ne nuisent-ils pas à la performance? Non - car en cas de succès, vous devez quand même faire toutes les vérifications (sinon vous ne vérifiez pas tous les cas d'erreur); et en cas d'échec, la plupart des compilateurs optimiseront la séquence des
if (oksofar)
vérifications échouées en un seul saut vers le code de nettoyage (GCC le fait certainement) - et dans tous les cas, le cas d'erreur est généralement moins critique pour les performances.
N'est-ce pas ajouter encore une autre variable? Dans ce cas oui, mais souvent la return_value
variable peut être utilisée pour jouer le rôle qui oksofar
joue ici. Si vous structurez vos fonctions pour renvoyer des erreurs de manière cohérente, vous pouvez même éviter la seconde if
dans chaque cas:
int return_value = 0;
if (!return_value) {
return_value = do_something(bar);
}
if (!return_value) {
return_value = init_stuff(bar);
}
if (!return_value) {
return_value = prepare_stuff(bar);
}
L'un des avantages d'un tel codage est que la cohérence signifie que tout endroit où le programmeur d'origine a oublié de vérifier la valeur de retour ressort comme un pouce endolori, ce qui facilite la recherche de (cette classe de) bogues.
Donc - c'est (encore) un autre style qui peut être utilisé pour résoudre ce problème. Utilisé correctement, il permet un code très propre et cohérent - et comme toute technique, entre de mauvaises mains, il peut finir par produire du code long et déroutant :-)