Perché usare try ... finalmente senza una clausola catch?


79

Il modo classico di programmare è con try ... catch. Quando è appropriato usare trysenza catch?

In Python appare legale e può avere senso:

try:
  #do work
finally:
  #do something unconditional

Tuttavia, il codice non ha fatto catchnulla. Allo stesso modo si potrebbe pensare in Java che sarebbe il seguente:

try {
    //for example try to get a database connection
}
finally {
  //closeConnection(connection)
}

Sembra buono e improvvisamente non devo preoccuparmi dei tipi di eccezione, ecc. Se questa è una buona pratica, quando è una buona pratica? In alternativa, quali sono i motivi per cui questa non è una buona pratica o non è legale? (Non ho compilato il sorgente. Lo sto chiedendo perché potrebbe essere un errore di sintassi per Java. Ho verificato che il Python sicuramente si compila.)

Un problema correlato che ho riscontrato è questo: continuo a scrivere la funzione / metodo, al termine del quale deve restituire qualcosa. Tuttavia, potrebbe trovarsi in un luogo che non dovrebbe essere raggiunto e deve essere un punto di ritorno. Quindi, anche se gestisco le eccezioni sopra, sto ancora ritornando NULLo una stringa vuota ad un certo punto del codice che non dovrebbe essere raggiunta, spesso la fine del metodo / funzione. Sono sempre riuscito a ristrutturare il codice in modo che non sia necessario return NULL, dal momento che sembra assolutamente meno di una buona pratica.


4
In Java, perché non inserire l'istruzione return alla fine del blocco try?
Kevin Cline

2
Sono triste che provare..finalmente e provare..catch entrambi usano la parola chiave try, a parte entrambi che iniziano con try sono 2 costrutti totalmente diversi.
Pieter B,

3
try/catchnon è "il modo classico di programmare". È il classico modo di programmare C ++ , perché in C ++ manca un costrutto try / finally, il che significa che è necessario implementare cambiamenti di stato reversibili garantiti usando brutti hack che coinvolgono RAII. Ma le lingue OO decenti non hanno questo problema, perché forniscono try / finalmente. È utilizzato per uno scopo molto diverso rispetto a try / catch.
Mason Wheeler,

3
Lo vedo molto con risorse di connessione esterne. Vuoi l'eccezione ma devi assicurarti di non lasciare una connessione aperta, ecc. Se la prendessi, in alcuni casi la ridisegneresti al livello successivo.
Rig

4
@MasonWheeler "Brutti hack" , per favore, spiega cosa c'è di male nell'avere un oggetto gestire la propria pulizia?
Baldrickk,

Risposte:


146

Dipende se è possibile gestire le eccezioni che possono essere sollevate a questo punto o meno.

Se riesci a gestire le eccezioni localmente dovresti, ed è meglio gestire l'errore il più vicino possibile al punto in cui viene generato.

Se non riesci a gestirli localmente, avere un try / finallyblocco è perfettamente ragionevole - supponendo che ci sia del codice che devi eseguire indipendentemente dal fatto che il metodo abbia avuto successo o meno. Ad esempio (dal commento di Neil ), l'apertura di uno stream e il passaggio di uno stream a un metodo interno da caricare è un ottimo esempio di quando sarebbe necessario try { } finally { }, utilizzando la clausola finally per assicurarsi che lo stream sia chiuso indipendentemente dal successo o fallimento della lettura.

Tuttavia, avrai comunque bisogno di un gestore delle eccezioni da qualche parte nel tuo codice, a meno che tu non voglia che l'applicazione si blocchi completamente, ovviamente. Dipende dall'architettura dell'applicazione esattamente dove si trova quel gestore.


8
"ed è meglio gestire l'errore il più vicino possibile al punto in cui viene generato." eh, dipende. Se riesci a recuperare e a completare la missione (per così dire), sì, certo. Se non ci riesci, meglio lasciare che l'eccezione arrivi fino in cima, dove sarà richiesto l'intervento (probabile) dell'utente per gestire l'accaduto.
Sarà

13
@will - ecco perché ho usato la frase "come possibile".
ChrisF

8
Buona risposta, ma aggiungerei un esempio: l'apertura di un flusso e il passaggio di tale flusso a un metodo interno da caricare è un ottimo esempio di quando ne avresti bisogno try { } finally { }, approfittando della clausola finally per garantire che il flusso alla fine sia chiuso indipendentemente dal successo / fallimento.
Neil,

perché a volte fino in cima è il più vicino possibile
Newtopian,

"Il solo tentativo di provare / finalmente il blocco è perfettamente ragionevole" cercavo esattamente questa risposta. grazie @ChrisF
neal aise,

36

Il finallyblocco viene utilizzato per il codice che deve sempre essere eseguito, indipendentemente dal fatto che si sia verificata una condizione di errore (eccezione).

Il codice nel finallyblocco viene eseguito dopo il trycompletamento del blocco e, se si è verificata un'eccezione rilevata, dopo il catchcompletamento del blocco corrispondente . Viene sempre eseguito, anche se si è verificata un'eccezione non rilevata nel blocco tryo catch.

Il finallyblocco viene in genere utilizzato per la chiusura di file, connessioni di rete, ecc. Che sono stati aperti nel tryblocco. Il motivo è che il file o la connessione di rete devono essere chiusi, se l'operazione che utilizza quel file o la connessione di rete ha avuto esito positivo o se non è riuscita.

È necessario prestare attenzione nel finallyblocco per assicurarsi che non generi un'eccezione. Ad esempio, assicurati doppiamente di controllare tutte le variabili per null, ecc.


8
+1: è idiomatico per "deve essere pulito". La maggior parte degli usi di try-finallypuò essere sostituita con withun'istruzione.
S. Lott,

12
Varie lingue hanno miglioramenti del try/finallycostrutto specifici per la lingua estremamente utili . C # ha using, Python ha with, ecc.
yfeldblum,

5
@yfeldblum - esiste una sottile differenza tra usinge try-finally, poiché il Disposemetodo non verrà chiamato dal usingblocco se si verifica un'eccezione nel IDisposablecostruttore dell'oggetto. try-finallyconsente di eseguire il codice anche se il costruttore dell'oggetto genera un'eccezione.
Scott Whitlock,

5
@ScottWhitlock: Questa è una buona cosa? Cosa stai cercando di fare, chiamare un metodo su un oggetto non costruito? Questo è un miliardo di tipi di cattivi.
DeadMG,


17

Un esempio in cui provare ... finalmente senza una clausola catch è appropriato (e ancora di più, idiomatico ) in Java è l'uso di Lock nel pacchetto di blocchi di utilità simultanee.

  • Ecco come viene spiegato e giustificato nella documentazione dell'API (il carattere in grassetto tra virgolette è mio):

    ... L'assenza di blocchi strutturati a blocchi rimuove il rilascio automatico dei blocchi che si verifica con metodi e istruzioni sincronizzati. Nella maggior parte dei casi, si dovrebbe usare il seguente linguaggio :

     Lock l = ...;
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }
    

    Quando si verificano il blocco e lo sblocco in ambiti diversi, è necessario assicurarsi che tutto il codice che viene eseguito mentre il blocco viene tenuto sia protetto da try-finally o try-catch per garantire che il blocco venga rilasciato quando necessario .


Posso mettere l.lock()dentro provare? try{ l.lock(); }finally{l.unlock();}
RMachnik,

1
tecnicamente, puoi. Non l'ho messo lì perché semanticamente, ha meno senso. tryin questo frammento ha lo scopo di racchiudere l'accesso alle risorse, perché inquinarlo con qualcosa di non correlato a questo
moscerino

4
Puoi, ma se l.lock()fallisce, il finallyblocco verrà comunque eseguito se si l.lock()trova all'interno del tryblocco. Se lo fai come suggerisce gnat, il finallyblocco verrà eseguito solo quando sappiamo che il blocco è stato acquisito.
Wtrmute,

12

A livello di base catche finallyrisolvere due problemi correlati ma diversi:

  • catch viene utilizzato per gestire un problema segnalato dal codice chiamato
  • finally viene utilizzato per ripulire i dati / risorse creati / modificati dal codice corrente, indipendentemente dal fatto che si sia verificato un problema o meno

Quindi entrambi sono in qualche modo legati a problemi (eccezioni), ma è praticamente tutto ciò che hanno in comune.

Una differenza importante è che il finallyblocco deve trovarsi nello stesso metodo in cui sono state create le risorse (per evitare perdite di risorse) e non può essere collocato a un livello diverso nello stack di chiamate.

Il catchtuttavia è una questione diversa: il posto giusto per esso dipende da dove si può effettivamente gestire l'eccezione. Non serve a nulla catturare un'eccezione in un luogo in cui non si può fare nulla al riguardo, quindi a volte è meglio lasciarlo semplicemente cadere.


2
Nitpick: "... il blocco finally deve essere nello stesso metodo in cui sono state create le risorse ..." . È sicuramente una buona idea farlo in questo modo, perché è più facile vedere che non ci sono perdite di risorse. Tuttavia, non è un prerequisito necessario; vale a dire che non è necessario farlo in quel modo. Puoi rilasciare la risorsa in finallyun'istruzione try (statica o dinamica) che racchiude ... e comunque essere al 100% a prova di perdite.
Stephen C

6

@yfeldblum ha la risposta corretta: try-finally senza una dichiarazione catch dovrebbe di solito essere sostituito con un costrutto linguistico appropriato.

In C ++, utilizza RAII e costruttori / distruttori; in Python è withun'affermazione; e in C #, è usingun'affermazione.

Questi sono quasi sempre più eleganti perché il codice di inizializzazione e finalizzazione si trovano in un posto (l'oggetto sottratto) piuttosto che in due punti.


2
E in Java sono blocchi ARM
MarkJ,

2
Supponendo di avere un oggetto mal progettato (ad esempio, uno che non implementa in modo appropriato IDisposable in C #) che non è sempre un'opzione praticabile.
Mootinator,

1
@mootinator: non puoi ereditare dall'oggetto mal progettato e ripararlo?
Neil G,

2
O incapsulamento? Bah. Voglio dire sì, certo.
Mootinator,

4

In molte lingue finallyun'istruzione viene eseguita anche dopo l'istruzione return. Questo significa che puoi fare qualcosa come:

try {
  // Do processing
  return result;
} finally {
  // Release resources
}

Che rilascia le risorse indipendentemente da come il metodo è stato terminato con un'eccezione o una dichiarazione di ritorno regolare.

Se questo è buono o cattivo è in discussione, ma try {} finally {}non è sempre limitato alla gestione delle eccezioni.


0

Potrei invocare l'ira di Pythonistas (non so perché non uso molto Python) o programmatori di altre lingue con questa risposta, ma a mio avviso la maggior parte delle funzioni non dovrebbe avere un catchblocco, idealmente parlando. Per dimostrare il perché, consentitemi di contrastarlo con la propagazione manuale dei codici di errore del tipo che ho dovuto fare quando ho lavorato con Turbo C alla fine degli anni '80 e all'inizio degli anni '90.

Diciamo quindi che abbiamo una funzione per caricare un'immagine o qualcosa del genere in risposta a un utente che seleziona un file di immagine da caricare, e questo è scritto in C e assembly:

inserisci qui la descrizione dell'immagine

Ho omesso alcune funzioni di basso livello ma possiamo vedere che ho identificato diverse categorie di funzioni, codificate per colore, in base alle responsabilità che hanno rispetto alla gestione degli errori.

Punto di errore e recupero

Ora non è mai stato difficile scrivere le categorie di funzioni che chiamo "possibili punti di throwerrore " (quelle che , ad esempio) e le funzioni "recupero e segnalazione errori" (quelle che catch, ad esempio).

Quelle funzioni erano sempre banali da scrivere correttamente prima che fosse disponibile la gestione delle eccezioni poiché una funzione che può incorrere in un errore esterno, come la mancata allocazione della memoria, può semplicemente restituire un NULLo 0o -1o impostare un codice di errore globale o qualcosa in tal senso. E il recupero / segnalazione degli errori è sempre stato facile poiché una volta che hai fatto un passo avanti nello stack delle chiamate fino a un punto in cui aveva senso recuperare e segnalare errori, prendi semplicemente il codice di errore e / o il messaggio e lo segnala all'utente. E naturalmente una funzione alla base di questa gerarchia che non può mai, mai fallire, non importa come sia cambiata in futuro ( Convert Pixel) è morta semplice da scrivere correttamente (almeno per quanto riguarda la gestione degli errori).

Propagazione dell'errore

Tuttavia, le noiose funzioni soggette all'errore umano sono state i propagatori di errori , quelli che non si sono imbattuti direttamente in errore ma hanno chiamato funzioni che potrebbero fallire da qualche parte più in profondità nella gerarchia. A quel punto, Allocate Scanlinepotrebbe dover gestire un guasto da malloce poi restituire un errore giù Convert Scanlines, quindi Convert Scanlinesavrebbe dovuto controllare tale errore e passarlo giù Decompress Image, poi Decompress Image->Parse Image, e Parse Image->Load Image, e Load Imageper il comando di utente-end in cui l'errore viene infine segnalato .

È qui che molti umani commettono errori poiché basta un solo propagatore di errori per non verificare e trasmettere l'errore perché l'intera gerarchia di funzioni si rovesci quando si tratta di gestire correttamente l'errore.

Inoltre, se i codici di errore vengono restituiti dalle funzioni, perdiamo praticamente la capacità, ad esempio, del 90% della nostra base di codice, di restituire valori di interesse in caso di successo poiché molte funzioni dovrebbero riservare il loro valore di ritorno per restituire un codice di errore su fallimento .

Riduzione dell'errore umano: codici di errore globali

Quindi, come possiamo ridurre la possibilità di errore umano? Qui potrei persino invocare l'ira di alcuni programmatori C, ma un miglioramento immediato secondo me è l'uso di codici di errore globali , come OpenGL con glGetError. Questo almeno libera le funzioni per restituire valori significativi di interesse in caso di successo. Esistono modi per rendere questo thread sicuro ed efficiente in cui il codice di errore è localizzato in un thread.

Ci sono anche alcuni casi in cui una funzione potrebbe incorrere in un errore, ma è relativamente innocuo che continui un po 'più a lungo prima che ritorni prematuramente a seguito della scoperta di un errore precedente. Ciò consente che ciò accada senza che sia necessario verificare la presenza di errori rispetto al 90% delle chiamate di funzione effettuate in ogni singola funzione, quindi può comunque consentire una corretta gestione degli errori senza essere così meticoloso.

Riduzione dell'errore umano: gestione delle eccezioni

Tuttavia, la soluzione di cui sopra richiede ancora così tante funzioni per gestire l'aspetto del flusso di controllo della propagazione manuale degli errori, anche se potrebbe aver ridotto il numero di righe di if error happened, return errortipo manuale di codice. Non lo eliminerebbe del tutto, poiché spesso dovrebbe esserci almeno un posto che controlla un errore e ritorna per quasi ogni singola funzione di propagazione dell'errore. Quindi questo è quando la gestione delle eccezioni entra in scena per salvare il giorno (sorta).

Ma il valore della gestione delle eccezioni è liberare la necessità di gestire l'aspetto del flusso di controllo della propagazione manuale degli errori. Ciò significa che il suo valore è legato alla capacità di evitare di dover scrivere un carico di catchblocchi di blocchi in tutta la base di codice. Nel diagramma sopra, l'unico posto che dovrebbe avere un catchblocco è Load Image User Commanddove viene segnalato l'errore. Nient'altro dovrebbe idealmente avere catchqualcosa perché altrimenti sta iniziando a diventare noioso e soggetto a errori come la gestione del codice di errore.

Quindi, se mi chiedi, se hai una base di codice che beneficia davvero della gestione delle eccezioni in modo elegante, dovrebbe avere il numero minimo di catchblocchi (per minimo non intendo zero, ma più simile a uno per ogni singolo high- operazione dell'utente finale che potrebbe non riuscire, e forse anche meno se tutte le operazioni dell'utente di fascia alta sono invocate attraverso un sistema di comando centrale).

Pulizia delle risorse

Tuttavia, la gestione delle eccezioni risolve solo la necessità di evitare di trattare manualmente gli aspetti del flusso di controllo della propagazione degli errori in percorsi eccezionali separati dai normali flussi di esecuzione. Spesso una funzione che funge da propagatore di errori, anche se lo fa automaticamente ora con EH, potrebbe ancora acquisire alcune risorse che deve distruggere. Ad esempio, una tale funzione potrebbe aprire un file temporaneo che deve chiudere prima di tornare dalla funzione, indipendentemente da cosa, o bloccare un mutex che deve sbloccare, qualunque cosa accada.

Per questo, potrei invocare l'ira di molti programmatori da tutti i tipi di lingue, ma penso che l'approccio C ++ a questo sia l'ideale. Il linguaggio introduce i distruttori che vengono invocati in modo deterministico nel momento in cui un oggetto esce dal campo di applicazione. Per questo motivo, il codice C ++ che, per esempio, blocca un mutex attraverso un oggetto mutex con ambito con un distruttore non ha bisogno di sbloccarlo manualmente, dal momento che verrà sbloccato automaticamente una volta che l'oggetto esce dall'ambito indipendentemente da ciò che accade (anche se un'eccezione è incontrato). Quindi non c'è davvero bisogno che codice C ++ ben scritto abbia mai a che fare con la pulizia delle risorse locali.

Nelle lingue prive di distruttori, potrebbe essere necessario utilizzare un finallyblocco per ripulire manualmente le risorse locali. Detto questo, batte ancora dover sporcare il codice con la propagazione manuale degli errori, a condizione che non si debbano fare catcheccezioni in tutto il mondo.

Inversione di effetti collaterali esterni

Questo è il problema concettuale più difficile da risolvere. Se una qualsiasi funzione, che si tratti di un propagatore di errori o di un punto di errore, causa effetti collaterali esterni, è necessario ripristinare o "annullare" tali effetti collaterali per riportare il sistema in uno stato come se l'operazione non si fosse mai verificata, anziché un " mezzo "valido" in cui l'operazione è riuscita a metà. Non conosco nessun linguaggio che renda questo problema concettuale molto più semplice, tranne i linguaggi che riducono semplicemente la necessità per la maggior parte delle funzioni di causare in primo luogo effetti collaterali esterni, come i linguaggi funzionali che ruotano attorno all'immutabilità e alle strutture di dati persistenti.

Qui finallyè probabilmente la soluzione più elegante là fuori al problema nei linguaggi che ruotano attorno alla mutabilità e agli effetti collaterali, perché spesso questo tipo di logica è molto specifica per una particolare funzione e non si adatta così bene al concetto di "pulizia delle risorse" ". E ti consiglio di usare finallyliberamente in questi casi per assicurarti che la tua funzione inverta gli effetti collaterali nelle lingue che la supportano, indipendentemente dal fatto che tu abbia bisogno o meno di un catchblocco (e ancora, se mi chiedi, un codice ben scritto dovrebbe avere il numero minimo di catchblocchi, e tutti i catchblocchi dovrebbero trovarsi nei punti in cui ha più senso come nel diagramma sopra in Load Image User Command).

Lingua dei sogni

Tuttavia, IMO finallyè vicino all'ideale per l'inversione degli effetti collaterali, ma non del tutto. Dobbiamo introdurre una booleanvariabile per ripristinare efficacemente gli effetti collaterali in caso di uscita prematura (da un'eccezione generata o meno), in questo modo:

bool finished = false;
try
{
    // Cause external side effects.
    ...

    // Indicate that all the external side effects were
    // made successfully.
    finished = true; 
}
finally
{
    // If the function prematurely exited before finishing
    // causing all of its side effects, whether as a result of
    // an early 'return' statement or an exception, undo the
    // side effects.
    if (!finished)
    {
        // Undo side effects.
        ...
    }
}

Se potessi mai progettare una lingua, il mio modo da sogno di risolvere questo problema sarebbe come questo per automatizzare il codice sopra:

transaction
{
    // Cause external side effects.
    ...
}
rollback
{
    // This block is only executed if the above 'transaction'
    // block didn't reach its end, either as a result of a premature
    // 'return' or an exception.

    // Undo side effects.
    ...
}

... con i distruttori per automatizzare la pulizia delle risorse locali, rendendola così di cui abbiamo solo bisogno transaction, rollbacke catch(anche se potrei ancora voler aggiungere finally, per esempio, lavorare con risorse C che non si ripuliscono). Tuttavia, finallycon una booleanvariabile è la cosa più vicina a rendere questo semplice che ho trovato finora privo del linguaggio dei miei sogni. La seconda soluzione più semplice che ho trovato per questo è guardie di ambito in linguaggi come C ++ e D, ma ho sempre trovato concettuali un po 'imbarazzanti concettualmente poiché offusca l'idea di "pulizia delle risorse" e "inversione degli effetti collaterali". Secondo me quelle sono idee molto distinte da affrontare in modo diverso.

Il mio sogno irrealizzabile di un linguaggio ruoterebbe anche pesantemente intorno all'immutabilità e alle strutture di dati persistenti per rendere molto più semplice, anche se non necessario, la scrittura di funzioni efficienti che non devono copiare in profondità strutture di dati di massa nella loro interezza, anche se la funzione causa nessun effetto collaterale.

Conclusione

Quindi comunque, a parte le mie divagazioni, penso che il tuo try/finallycodice per chiudere il socket sia perfetto e ottimo considerando che Python non ha l'equivalente C ++ dei distruttori, e personalmente penso che dovresti usarlo liberamente per i luoghi che hanno bisogno di invertire gli effetti collaterali e ridurre al minimo il numero di luoghi in cui è necessario catchraggiungere i luoghi in cui ha più senso.


-66

Catturare errori / eccezioni e gestirli in modo accurato è altamente raccomandato anche se non obbligatorio.

Il motivo per cui lo dico è perché credo che ogni sviluppatore dovrebbe conoscere e affrontare il comportamento della propria candidatura, altrimenti non ha completato il proprio lavoro in modo adeguato. Non esiste una situazione in cui un blocco try-finally sostituisce il blocco try-catch-finally.

Ti fornirò un semplice esempio: supponi di aver scritto il codice per caricare file sul server senza rilevare eccezioni. Ora, se per qualche motivo il caricamento non riesce, il client non saprà mai cosa è andato storto. Ma, se hai riscontrato l'eccezione, puoi visualizzare un messaggio di errore che spiega cosa è andato storto e come può l'utente rimediare.

Regola d'oro: cogli sempre l'eccezione, perché indovinare richiede tempo


22
-1: in Java, potrebbe essere necessaria una clausola finally per rilasciare risorse (ad es. Chiudere un file o rilasciare una connessione DB). Ciò è indipendente dalla capacità di gestire un'eccezione.
Kevin Cline il

@kevincline, Non sta chiedendo se usare o meno finalmente ... Tutto quello che sta chiedendo è se è richiesta la cattura di un'eccezione o meno .... Sa cosa provare, catturare e infine ..... Alla fine è il parte essenziale, lo sappiamo tutti e perché è usato ....
Pankaj Upadhyay,

63
@Pankaj: la tua risposta suggerisce che una catchclausola dovrebbe essere sempre presente ogni volta che c'è un try. I collaboratori più esperti, incluso me, credono che questo sia un consiglio scadente. Il tuo ragionamento è imperfetto. Il metodo contenente il trynon è l'unico posto possibile in cui l'eccezione può essere catturata. Spesso è più semplice e preferibile consentire la cattura e segnalare eccezioni dal livello più elevato, piuttosto che duplicare le clausole di cattura in tutto il codice.
Kevin Cline il

@kevincline, credo che la percezione della domanda da parte degli altri sia leggermente diversa. La domanda era precisamente: dovremmo prendere un'eccezione o no ... E ho risposto a questo proposito. Gestire l'eccezione può essere fatto in vari modi e non solo per provare finalmente. Ma questa non era la preoccupazione di OP. Se sollevare un'eccezione è abbastanza buono per te e gli altri ragazzi, allora devo dire buona fortuna.
Pankaj Upadhyay,

Con le eccezioni, si desidera interrompere la normale esecuzione delle istruzioni (e senza verificare manualmente la riuscita di ogni passaggio). Questo è particolarmente vero con le chiamate di metodo profondamente annidate: un metodo di 4 livelli all'interno di alcune librerie non può semplicemente "ingoiare" un'eccezione; deve essere rigettato attraverso tutti gli strati. Naturalmente, ogni livello potrebbe racchiudere l'eccezione e aggiungere ulteriori informazioni; questo spesso non viene fatto per vari motivi, il tempo aggiuntivo per lo sviluppo e la verbosità inutile sono i due più grandi.
Daniel B,
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.