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 catch
blocco, 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:
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 throw
errore " (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 NULL
o 0
o -1
o 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 Scanline
potrebbe dover gestire un guasto da malloc
e poi restituire un errore giù Convert Scanlines
, quindi Convert Scanlines
avrebbe dovuto controllare tale errore e passarlo giù Decompress Image
, poi Decompress Image->Parse Image
, e Parse Image->Load Image
, e Load Image
per 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 error
tipo 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 catch
blocchi di blocchi in tutta la base di codice. Nel diagramma sopra, l'unico posto che dovrebbe avere un catch
blocco è Load Image User Command
dove viene segnalato l'errore. Nient'altro dovrebbe idealmente avere catch
qualcosa 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 catch
blocchi (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 finally
blocco 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 catch
eccezioni 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 finally
liberamente 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 catch
blocco (e ancora, se mi chiedi, un codice ben scritto dovrebbe avere il numero minimo di catch
blocchi, e tutti i catch
blocchi 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 boolean
variabile 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
, rollback
e catch
(anche se potrei ancora voler aggiungere finally
, per esempio, lavorare con risorse C che non si ripuliscono). Tuttavia, finally
con una boolean
variabile è 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/finally
codice 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 catch
raggiungere i luoghi in cui ha più senso.