L'uso della clausola finally per fare un lavoro dopo il ritorno è cattivo / pericoloso?


30

Durante la scrittura di un iteratore, mi sono trovato a scrivere il seguente pezzo di codice (eliminazione della gestione degli errori)

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

trovandolo leggermente più facile da leggere rispetto a

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

So che è un semplice esempio, in cui la differenza nella leggibilità potrebbe non essere così schiacciante, ma sono interessato all'opinione generale sul fatto che sia male usare try-finally in casi come questo in cui non ci sono eccezioni, oppure se è effettivamente preferito quando semplifica il codice.

Se è male: perché? Stile, prestazioni, insidie, ...?

Conclusione Grazie a tutte le vostre risposte! Immagino che la conclusione (almeno per me) sia che il primo esempio avrebbe potuto essere più leggibile se fosse un modello comune, ma non lo è. Pertanto, la confusione introdotta usando un costrutto al di fuori del suo scopo, insieme a un flusso di eccezioni forse offuscato, supererà qualsiasi semplificazione.


10
Personalmente sarei in grado di capire il secondo più del primo, ma raramente uso i finallyblocchi.
Christian Mann,

2
return current = fetcher.fetchNext (corrente); // Che ne dici di questo, alias hai davvero bisogno del prefetch?
scarfridge,

1
@scarfridge L'esempio riguarda l'implementazione Iterator, dove in effetti hai bisogno di una sorta di prefetch per hasNext()funzionare. Provate voi stessi.
maaartinus,

1
@maaartinus Ne sono consapevole. A volte hasNext () restituisce semplicemente true, ad esempio la sequenza hailstone e talvolta recuperare l'elemento successivo è proibitivo. In quest'ultimo caso dovresti ricorrere al recupero dell'elemento solo su richiesta simile all'inizializzazione lazy.
scarfridge,

1
@scarfridge Il problema con l'uso comune di Iteratorè che è necessario recuperare il valore hasNext()(perché recuperarlo è abbastanza spesso l'unico modo per scoprire se esiste) e restituirlo next()come ha fatto l'OP.
maaartinus,

Risposte:


18

Personalmente, avrei pensato all'idea della separazione delle query dei comandi . Quando si arriva fino ad esso, next () ha due scopi: lo scopo pubblicizzato di recuperare l'elemento successivo e l'effetto collaterale nascosto di mutare lo stato interno di next. Quindi, quello che stai facendo è eseguire lo scopo pubblicizzato nel corpo del metodo e quindi affrontare un effetto collaterale nascosto in una clausola finally, che sembra ... imbarazzante, sebbene non "errata", esattamente.

Ciò che si riduce davvero è quanto sia comprensibile il codice, direi, e in questo caso la risposta è "meh". Stai "provando" una semplice dichiarazione di ritorno e quindi eseguendo un effetto collaterale nascosto in un blocco di codice che dovrebbe essere per il recupero degli errori a prova di errore. Quello che stai facendo è 'intelligente', e il codice 'intelligente' spesso induce i manutentori a borbottare, "che cosa ... oh, penso di averlo capito." Meglio che le persone che leggono il tuo codice borbottino "sì, uh-huh, ha senso, sì ..."

Quindi, cosa succede se si separa la mutazione di stato dalle chiamate degli accessor? Immagino che il problema della leggibilità di cui ti occupi diventa discutibile, ma non so come questo influisca sulla tua più ampia astrazione. Qualcosa da considerare, comunque.


1
+1 per il commento "intelligente". Questo cattura molto accuratamente questo esempio.

2
L'azione "return and anticipo" di next () è forzata sull'OP dall'interfaccia iteratrice Java ingombrante.
Kevin Cline,

37

Puramente dal punto di vista dello stile, penso a queste tre linee:

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

... sono entrambi più evidenti e più brevi di un blocco try / finally. Dato che non ti aspetti che vengano generate eccezioni, l'uso di un tryblocco confonderà le persone.


2
Un blocco try / catch / finally potrebbe anche aggiungere un sovraccarico aggiuntivo, non sono sicuro che javac o il compilatore JIT li eliminino anche se non stai generando un'eccezione.
Martijn Verburg,

2
@MartijnVerburg sì, javac aggiunge ancora una voce alla tabella delle eccezioni e duplica il codice di fine anche se il blocco try è vuoto.
Daniel Lubarov,

@Daniel - grazie ero troppo pigro per eseguire javap :-)
Martijn Verburg

@Martijn Verburg: AFAIK, un blocco try-catch-finallt è essenzialmente gratuito fintanto che non viene generata alcuna eccezione effettiva. Qui, javac non ci prova e non ha bisogno di ottimizzare nulla in quanto JIT se ne occuperà.
maaartinus,

@maaartinus - grazie che ha senso - ho bisogno di leggere di nuovo il codice sorgente OpenJDK :-)
Martijn Verburg

6

Ciò dipende dallo scopo del codice all'interno del blocco finally. L'esempio canonico sta chiudendo un flusso dopo aver letto / scritto da esso, una sorta di "pulizia" che deve essere sempre fatta. Ripristinare un oggetto (in questo caso un iteratore) su uno stato valido IMHO conta anche come cleanup, quindi non vedo alcun problema qui. Se OTOH stavi usando returnnon appena è stato trovato il tuo valore di ritorno e aggiungendo un sacco di codice non correlato nel blocco finally, allora oscurerebbe lo scopo e renderebbe tutto meno comprensibile.

Non vedo alcun problema nell'usarlo quando "non ci sono eccezioni". È molto comune usare try...finallysenza un catchquando il codice può solo essere lanciato RuntimeExceptione non hai intenzione di gestirli. A volte, finalmente è solo una salvaguardia, e sai per la logica del tuo programma che non verrà mai generata alcuna eccezione (la classica condizione "questo non dovrebbe mai accadere").

Insidie: qualsiasi eccezione sollevata all'interno del tryblocco farà finallycorrere il bock. Questo può metterti in uno stato incoerente. Quindi, se la tua dichiarazione di ritorno fosse simile a:

return preProcess(next);

e questo codice ha sollevato un'eccezione, fetchNextverrebbe comunque eseguito. OTOH se lo hai codificato come:

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

quindi non funzionerebbe. So che stai assumendo che il codice di prova non possa mai sollevare alcuna eccezione, ma per casi più complessi di questo come puoi esserne sicuro? Se il tuo iteratore fosse un oggetto di lunga durata, continuerebbe a esistere anche se si verificasse un errore irreversibile nel thread corrente, quindi sarebbe importante mantenerlo in uno stato valido in ogni momento. Altrimenti, non importa molto ...

Prestazioni: sarebbe interessante decompilare tale codice per vedere come funziona sotto il cofano, ma non conosco abbastanza della JVM per nemmeno fare una buona ipotesi ... Le eccezioni sono di solito "eccezionali", quindi il codice che gestisce non hanno bisogno di essere ottimizzati per la velocità (da qui il consiglio di non usare mai le eccezioni nel normale flusso di controllo dei tuoi programmi), ma non lo so finally.


Non stai davvero fornendo una ragione abbastanza buona per evitare di usarlo finallyin questo modo, allora? Intendo, come dici tu, il codice finisce per avere conseguenze indesiderate; peggio ancora, probabilmente non stai nemmeno pensando alle eccezioni.
Andres F.

2
La normale velocità del flusso di controllo non è influenzata in modo significativo dai costrutti che gestiscono le eccezioni. La penalità di prestazione viene pagata solo quando si lanciano e si prendono eccezioni, non quando si entra in un blocco try o si entra in un blocco finally durante il normale funzionamento.
yfeldblum,

1
@AndresF. Vero, ma è valido anche per qualsiasi codice di pulizia. Ad esempio, supponiamo che un riferimento a un flusso aperto sia impostato nullda un codice buggy; il tentativo di leggere da esso genererà un'eccezione, il tentativo di chiuderlo nel finallyblocco solleverà un'altra eccezione. Inoltre, questa è una situazione in cui lo stato del calcolo non ha importanza, si desidera chiudere il flusso indipendentemente dal fatto che si sia verificata un'eccezione. Potrebbero esserci anche altre situazioni del genere. Per questo motivo lo sto trattando come una trappola per gli usi validi, piuttosto che come consiglio di non usarlo mai.
mgibsonbr,

C'è anche il problema che quando sia il tentativo che il blocco finale generano un'eccezione, l'eccezione nel tentativo non viene mai vista da nessuno, ma è probabilmente la causa del problema.
Hans-Peter Störr,

3

La frase del titolo: "... finalmente la clausola per fare un lavoro dopo il ritorno ..." è falsa. Il blocco finally si verifica prima che la funzione ritorni. Questo è il punto finale di fatto.

Quello che stai abusando qui è l'ordine di valutazione, in cui il valore di next viene memorizzato per essere restituito prima di modificarlo. Non è una pratica comune e secondo me sbagliata in quanto ti rende il codice non sequenziale e quindi molto più difficile da seguire.

L'uso previsto dei blocchi finally è per la gestione delle eccezioni in cui devono verificarsi alcune conseguenze indipendentemente dalle eccezioni generate, ad esempio pulizia di alcune risorse, chiusura della connessione DB, chiusura di un file / socket ecc.


+1, aggiungerei che anche se le specifiche della lingua garantiscono che il valore viene memorizzato prima che venga restituito, va pietosamente contro le aspettative del lettore e quindi dovrebbe essere evitato quando ragionevolmente possibile. Il debug è due volte più difficile della codifica ...
jmoreno,

0

Starei molto attento, perché (in generale, non nel tuo esempio) una parte in try può generare un'eccezione, e se viene espulso, nulla verrà restituito e se in realtà hai intenzione di eseguire ciò che è finalmente dopo il ritorno, lo farà esegui quando non lo volevi.

E un'altra lattina di worm è che, in generale, qualche utile azione alla fine può generare eccezioni, quindi puoi finire con un metodo davvero disordinato.

Per questo motivo, qualcuno che lo legge deve pensare a questi problemi, quindi è più difficile da capire.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.