Un'eccezione dovrebbe essere descrittiva. Questa è una combinazione di tre cose:
- L'identità (tipo) dell'eccezione.
- Il messaggio contenuto nell'eccezione.
- Altri metadati contenuti nell'eccezione, ad esempio un'eccezione concatenata o un codice di errore.
Si noti che lingue e runtime diversi hanno semantica delle eccezioni leggermente diversa, ma queste tre idee sono abbastanza universali. Per lo meno, un'eccezione ha un tipo e di solito ha un messaggio, ma i dati aggiuntivi non sono sempre pertinenti. Nota anche che alcuni linguaggi come C ++ non hanno un tipo di eccezione "root": mentre C ++ ha std::exception
, uno può lanciare qualsiasi incluse le primitive.
Scegliendo Java, ci sono quattro tipi di core che formano la dorsale della gerarchia delle eccezioni: Throwable
, Error
, Exception
e RuntimeException
. Mentre nessuno è astratto, nessuno deve essere gettato direttamente: si dovrebbe usare un tipo più specifico. Detto questo, il fatto che nessuno sia astratto significa che è possibile istanziarlo e lanciarli direttamente.
Ritorno al punto originale: generalmente, si dovrebbero generare sottotipi di eccezioni specifici perché rende più facile che determinate eccezioni ingigantiscano lo stack di chiamate per essere catturato al livello appropriato. Considera il seguente esempio Java:
public void doSomething() throws Exception {
doOneThing(); // Throws Exception
doAnotherThing(); // Throws IOException
}
Se doSomething()
non è un luogo appropriato per gestire l'eccezione, il metodo che lo chiama deve sapere quali eccezioni prendere. Anche l'ordine delle clausole catch
è importante: devono passare dallo specifico al generale. La cattura di Exception
prima ingoierà tutte le eccezioni tutte , incluso IOException
, nello stesso blocco. Questo diventa un po 'più difficile da capire cosa catturare e cosa fare con l'eccezione una volta catturati. Considera invece questo codice:
public void doSomething() throws SQLException, IOException {
doOneThing(); // Throws SQLException
doAnotherThing(); // Throws IOException
}
Ora è molto meno ambiguo quali siano gli errori possibili e non vi è alcun accoppiamento in termini di ordine in cui vengono catturati. Invece di questo:
try {
doSomething();
}
// Order matters!
catch (IOException e) {
// Obviously some sort of file or socket I/O error.
}
catch (Exception e) {
// What are we dealing with here?
}
Abbiamo questo:
try {
doSomething();
}
catch (IOException e) {
// Obviously some sort of file or socket I/O error.
}
catch (SQLException e) {
// Now we can get the error code and do something meaningful.
}
Puoi anche consentire ad alcune eccezioni di passare attraverso lo stack di chiamate, e altre a essere gestite in quel blocco try / catch. Ad esempio, il primo esempio renderebbe difficile consentire a SQLException di superare lo stack di chiamate durante la gestione di IOException, perché non è dichiarato correttamente. Anche se possibile, richiederebbe il casting, il controllo dei tipi e sarebbe un codice fragile e non pulito.
Il secondo esempio è più chiaro in termini di intenzioni e consente di eseguire operazioni più utili utilizzando l'oggetto eccezione.
Si noti che gli esempi precedenti fanno sì che si desideri lasciare passare alcune eccezioni, oppure si potrebbe voler gestire eccezioni diverse in modo diverso (ad esempio mostrare un messaggio di errore per uno, ritentare l'operazione per un altro). Alcune lingue consentono di catturare più tipi di eccezioni senza catturare eccezioni tutte . Questo può essere un buon compromesso tra un blocco catch-all e un blocco molto verboso "catch this, then this, then ...".
Ad esempio, considera il seguente blocco:
try {
// open a file, read some XML, and instantiate
// some classes referenced therein using reflection.
initializeModule();
}
catch (IOException e) {
// Do something
}
catch (ReflectiveOperationException e) {
// Do something
}
catch (XMLStreamException e) {
// Do something
}
Java 8 consente invece quanto segue:
try {
// open a file, read some XML, and instantiate
// some classes referenced therein using reflection.
initializeModule();
}
catch (IOException | ReflectiveOperationException | XMLStreamException e) {
// Do something
}
Questo può essere un buon compromesso perché cattura eccezioni specifiche in un singolo blocco senza che cattura troppo, il che potrebbe non essere un progetto pulito o il posto giusto per catturare altre eccezioni. Ad esempio, il codice precedente consente alle classi RuntimeException (ad esempio, i puntatori nulli) di superare lo stack di chiamate. Questo può essere fastidioso da fare in Java perché RuntimeException eredita da Exception. L'eccezione di cattura rileverà anche RuntimeExceptions, che non è sempre ciò che si vuole fare.
L'unica volta in cui trovo che tutte le eccezioni sono buone pratiche è l'ultima risorsa prima di morire:
public static void main(String[] args) {
try {
// Invoke program logic here.
}
catch (Throwable t) {
// Last-ditch logging before letting the program die.
}
}