Sono sorpreso che nessuno abbia suggerito questa alternativa, quindi anche se la domanda è in circolazione da un po 'di tempo, la aggiungerò: un buon modo per affrontare questo problema è usare le variabili per tenere traccia dello stato corrente. Questa è una tecnica che può essere utilizzata indipendentemente dal fatto che goto
venga utilizzata per arrivare al codice di pulizia. Come ogni tecnica di codifica, ha pro e contro e non sarà adatta a ogni situazione, ma se stai scegliendo uno stile vale la pena considerare, soprattutto se vuoi evitare goto
senza finire con if
s nidificate profondamente .
L'idea di base è che, per ogni azione di pulizia che potrebbe essere necessario intraprendere, c'è una variabile dal cui valore possiamo dire se la pulizia deve essere eseguita o meno.
Mostrerò goto
prima la versione, perché è più vicina al codice nella domanda originale.
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 vantaggio di questo rispetto ad alcune delle altre tecniche è che, se l'ordine delle funzioni di inizializzazione viene modificato, la pulizia corretta continuerà ad essere eseguita - ad esempio, utilizzando il switch
metodo descritto in un'altra risposta, se l'ordine di inizializzazione cambia, allora il switch
deve essere modificato con molta attenzione per evitare di provare a ripulire qualcosa che non è stato effettivamente inizializzato in primo luogo.
Ora, alcuni potrebbero obiettare che questo metodo aggiunge molte variabili extra - e in effetti in questo caso è vero - ma in pratica spesso una variabile esistente traccia già, o può essere fatta per tracciare, lo stato richiesto. Ad esempio, se prepare_stuff()
è effettivamente una chiamata a malloc()
, o a open()
, è possibile utilizzare la variabile che contiene il puntatore o il descrittore di file restituito, ad esempio:
int fd = -1;
....
fd = open(...);
if (fd == -1) {
goto cleanup;
}
...
cleanup:
if (fd != -1) {
close(fd);
}
Ora, se monitoriamo ulteriormente lo stato di errore con una variabile, possiamo evitarlo del goto
tutto e comunque ripulirlo correttamente, senza avere un rientro che diventa sempre più profondo quanto maggiore è l'inizializzazione di cui abbiamo bisogno:
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;
}
Ancora una volta, ci sono potenziali critiche su questo:
- Non tutti quei "se" fanno male alla performance? No, perché in caso di successo, devi comunque fare tutti i controlli (altrimenti non stai controllando tutti i casi di errore); e in caso di errore la maggior parte dei compilatori ottimizzerà la sequenza dei
if (oksofar)
controlli falliti a un singolo salto al codice di pulizia (GCC lo fa certamente) - e in ogni caso, il caso di errore è solitamente meno critico per le prestazioni.
Non sta aggiungendo ancora un'altra variabile? In questo caso sì, ma spesso la return_value
variabile può essere utilizzata per interpretare il ruolo che oksofar
sta giocando qui. Se strutturi le tue funzioni per restituire errori in modo coerente, puoi persino evitare il secondo if
in ogni caso:
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);
}
Uno dei vantaggi di una codifica del genere è che la coerenza significa che ogni punto in cui il programmatore originale ha dimenticato di controllare il valore restituito sporge come un pollice dolente, rendendo molto più facile trovare (quella classe di) bug.
Quindi, questo è (ancora) uno stile in più che può essere utilizzato per risolvere questo problema. Usato correttamente consente un codice molto pulito e coerente - e come ogni tecnica, nelle mani sbagliate può finire per produrre codice prolisso e confuso :-)