Va bene usare le eccezioni come strumenti per "rilevare" gli errori in anticipo?


29

Uso le eccezioni per rilevare in anticipo i problemi. Per esempio:

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

Il mio programma non dovrebbe mai passare nullin questa funzione. Non ho mai intenzione di farlo. Tuttavia, come tutti sappiamo, nella programmazione accadono cose indesiderate.

Generare un'eccezione se si verifica questo problema, mi consente di individuarlo e risolverlo, prima che causi più problemi in altri punti del programma. L'eccezione interrompe il programma e mi dice "sono successe cose brutte qui, risolvile". Invece di nullspostarsi all'interno del programma causando problemi altrove.

Ora, hai ragione, in questo caso nullsarebbe semplicemente una causa NullPointerExceptionimmediata, quindi potrebbe non essere il miglior esempio.

Ma considera un metodo come questo per esempio:

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

In questo caso, un nullparametro as verrebbe passato al programma e potrebbe causare errori molto più tardi, che sarà difficile risalire alla loro origine.

Cambiando la funzione in questo modo:

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

Mi permette di individuare il problema molto prima che causi strani errori in altri luoghi.

Inoltre, un nullriferimento come parametro è solo un esempio. Potrebbero esserci molti tipi di problemi, da argomenti non validi a qualsiasi altra cosa. È sempre meglio individuarli presto.


Quindi la mia domanda è semplicemente: è questa buona pratica? Il mio uso delle eccezioni come strumenti di prevenzione dei problemi è buono? Si tratta di un'applicazione legittima di eccezioni o è problematica?


2
Il downvoter può spiegare cosa c'è di sbagliato in questa domanda?
Giorgio,

2
"crash presto, crash spesso"
Bryan Chen

16
Questo è letteralmente il motivo per cui ci sono eccezioni.
raptortech97,

Si noti che i contratti di codifica consentono di rilevare staticamente molti di questi errori. Questo è generalmente superiore al lancio di un'eccezione in fase di esecuzione. Il supporto e l'efficacia dei contratti di codifica variano in base alla lingua.
Brian,

3
Dato che stai lanciando IllegalArgumentException, è ridondante dire " Input person".
Kevin Cline,

Risposte:


29

Sì, "fallire presto" è un ottimo principio, e questo è semplicemente un modo possibile per attuarlo. E nei metodi che devono restituire un valore specifico, non c'è davvero molto altro che si può fare per fallire deliberatamente: si tratta di lanciare eccezioni o innescare asserzioni. Si presume che le eccezioni segnalino condizioni "eccezionali" e che rilevare un errore di programmazione è certamente eccezionale.


Fallire presto è la strada da percorrere se non c'è modo di recuperare da un errore o una condizione. Ad esempio, se è necessario copiare un file e il percorso di origine o destinazione è vuoto, è necessario generare immediatamente un'eccezione.
Michael Shopsin

È anche noto come "fail fast"
keuleJ

8

Sì, lanciare eccezioni è una buona idea. Gettali presto, gettali spesso, gettali avidamente.

So che esiste un dibattito sulle "eccezioni e asserzioni", con alcuni tipi di comportamento eccezionale (in particolare quelli che si ritiene riflettano errori di programmazione) gestiti da asserzioni che possono essere "compilate" per il runtime piuttosto che build di debug / test. Ma la quantità di prestazioni consumata in alcuni controlli di correttezza extra è minima sull'hardware moderno e qualsiasi costo aggiuntivo è di gran lunga superiore al valore di avere risultati corretti e non corrotti. In realtà non ho mai incontrato una base di codice dell'applicazione per la quale vorrei (la maggior parte) controlli rimossi in fase di esecuzione.

Sono tentato di dire che non vorrei un sacco di ulteriori controlli e condizioni all'interno degli stretti cicli di codice numericamente intenso ... ma è proprio lì che vengono generati molti errori numerici e, se non rilevati lì, si propagheranno all'esterno effettuare tutti i risultati. Quindi anche lì, vale la pena fare controlli. In effetti, alcuni dei migliori algoritmi numerici più efficienti si basano sulla valutazione degli errori.

Un ultimo posto per essere molto consapevoli del codice aggiuntivo è il codice molto sensibile alla latenza, in cui i condizionali extra possono causare stalle della pipeline. Quindi, nel mezzo del sistema operativo, DBMS e altri kernel middleware e gestione della comunicazione / protocollo di basso livello. Ma ancora una volta, questi sono alcuni dei luoghi in cui è più probabile che vengano osservati errori e che i loro effetti (sicurezza, correttezza e integrità dei dati) siano più dannosi.

Un miglioramento che ho riscontrato è di non generare solo eccezioni a livello di base. IllegalArgumentExceptionè buono, ma può provenire essenzialmente da qualsiasi luogo. Non ci vuole molto nella maggior parte delle lingue per aggiungere eccezioni personalizzate. Per il modulo di gestione delle persone, dire:

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

Quindi quando qualcuno vede un PersonArgumentException, è chiaro da dove proviene. C'è un atto di bilanciamento su quante eccezioni personalizzate vuoi aggiungere, perché non vuoi moltiplicare inutilmente le entità (Occam's Razor). Spesso bastano poche eccezioni personalizzate per segnalare "questo modulo non sta ottenendo i dati giusti!" o "questo modulo non può fare quello che dovrebbe fare!" in un modo specifico e su misura, ma non così preciso che è necessario implementare nuovamente la gerarchia di eccezioni. Vengo spesso a quel piccolo insieme di eccezioni personalizzate iniziando con le eccezioni di borsa, quindi scansionando il codice e realizzando che "questi N posti stanno aumentando le eccezioni di borsa, ma si riducono all'idea di livello superiore che non stanno ottenendo i dati hanno bisogno; lasciamo


I've never actually met an application codebase for which I'd want (most) checks removed at runtime. Quindi non hai eseguito il codice critico per le prestazioni. Sto lavorando a qualcosa in questo momento che fa 37 milioni di operazioni al secondo con affermazioni compilate su e 42 milioni senza di esse. Le asserzioni non sono valide per l'input esterno, ma sono lì per assicurarsi che il codice sia corretto. I miei clienti sono più che felici di aumentare del 13% una volta che sono soddisfatto che le mie cose non siano rotte.
Blrfl,

Il peppering di una base di codice con eccezioni personalizzate dovrebbe essere evitato perché, sebbene possa aiutarti, non aiuta gli altri che devono conoscerli. Di solito se ti ritrovi a estendere un'eccezione comune e non aggiungere membri o funzionalità, in realtà non ne hai bisogno. Anche nel tuo esempio, PersonArgumentExceptionnon è chiaro come IllegalArgumentException. Quest'ultimo è universalmente noto per essere lanciato quando viene sostenuta una discussione illegale. In realtà mi aspetto che il primo venga lanciato se a Personera in uno stato non valido per una chiamata (simile a InvalidOperationExceptionin C #).
Selali Adobor,

È vero che se non stai attento puoi attivare la stessa eccezione da qualche altra parte nella funzione, ma questo è un difetto del codice, non della natura delle eccezioni comuni. Ed è qui che entrano in gioco le tracce di debug e stack. Se stai cercando di "etichettare" le tue eccezioni, usa le strutture esistenti che forniscono le eccezioni più comuni, come usare il costruttore di messaggi (è esattamente quello per cui è lì). Alcune eccezioni forniscono persino ai costruttori parametri aggiuntivi per ottenere il nome dell'argomento problematico, ma è possibile fornire alla stessa struttura un messaggio ben formato.
Selali Adobor,

Ammetto che il semplice rebranding di un'eccezione comune a un'etichetta privata sostanzialmente la stessa eccezione è un esempio debole e raramente vale la pena. In pratica, provo a emettere eccezioni personalizzate a un livello semantico più elevato, più intimamente legato all'intento del modulo.
Jonathan Eunice,

Ho svolto un lavoro sensibile alle prestazioni nei settori numerico / HPC e OS / middleware. Un aumento delle prestazioni del 13% non è cosa da poco. Potrei disattivare alcuni controlli per ottenerlo, allo stesso modo in cui un comandante militare potrebbe chiedere il 105% della potenza nominale del reattore. Ma ho visto "funzionerà più velocemente in questo modo" dato spesso come motivo per disattivare controlli, backup e altre protezioni. Fondamentalmente si tratta di compromettere la resilienza e la sicurezza (in molti casi, inclusa l'integrità dei dati) per prestazioni extra. Se questo vale la pena è una chiamata di giudizio.
Jonathan Eunice,

3

Fallire il prima possibile è ottimo quando si esegue il debug di un'applicazione. Ricordo un particolare errore di segmentazione in un programma C ++ legacy: il luogo in cui il bug veniva rilevato non aveva nulla a che fare con il luogo in cui era stato introdotto (il puntatore null veniva spostato felicemente da un posto all'altro in memoria prima che alla fine causasse un problema ). Le tracce dello stack non possono aiutarti in questi casi.

Quindi sì, la programmazione difensiva è un approccio davvero efficace per rilevare e correggere rapidamente i bug. D'altra parte, può essere esagerato, soprattutto con riferimenti null.

Nel tuo caso specifico, ad esempio: se uno qualsiasi dei riferimenti è nullo, NullReferenceExceptionverrà lanciato alla frase successiva, quando si cerca di ottenere l'età di una persona. Non hai davvero bisogno di controllare le cose da solo qui: lascia che il sistema sottostante rilevi quegli errori e generi eccezioni, ecco perché esistono .

Per un esempio più realistico, è possibile utilizzare le assertistruzioni che:

  1. Sono più brevi per scrivere e leggere:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. Sono progettati specificamente per il tuo approccio. In un mondo in cui hai sia affermazioni che eccezioni, puoi distinguerle come segue:

    • Le asserzioni riguardano errori di programmazione ("/ * questo non dovrebbe mai accadere * /"),
    • Le eccezioni sono per casi angolari (situazioni eccezionali ma probabili)

Pertanto, esponendo le tue assunzioni sugli input e / o lo stato della tua applicazione con asserzioni, lascia che lo sviluppatore successivo comprenda un po 'di più lo scopo del tuo codice.

Anche un analizzatore statico (ad esempio il compilatore) potrebbe essere più felice.

Infine, le asserzioni possono essere rimosse dall'applicazione distribuita utilizzando un singolo switch. Ma in generale, non aspettatevi di migliorare l'efficienza con questo: i controlli delle asserzioni in fase di esecuzione sono trascurabili.


1

Per quanto ne so, programmatori diversi preferiscono una soluzione o l'altra.

La prima soluzione è di solito preferita perché è più concisa, in particolare, non è necessario controllare più volte la stessa condizione in funzioni diverse.

Trovo la seconda soluzione, ad es

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

più solido, perché

  1. Rileva l'errore il prima possibile, ovvero quando registerPerson()viene chiamato, non quando viene generata un'eccezione puntatore null da qualche parte nello stack di chiamate. Il debug diventa molto più semplice: sappiamo tutti fino a che punto un valore non valido può viaggiare attraverso il codice prima che si manifesti come un bug.
  2. Riduce l'accoppiamento tra le funzioni: registerPerson()non fa ipotesi su quali altre funzioni finiranno per usare l' personargomento e su come lo useranno: la decisione che nullè un errore viene presa e implementata localmente.

Quindi, specialmente se il codice è piuttosto complesso, tendo a preferire questo secondo approccio.


1
Fornire il motivo ("Input person is null.") Aiuta anche a capire il problema, diversamente da quando un metodo oscuro in una libreria fallisce.
Florian F,

1

In generale, sì, è una buona idea "fallire presto". Tuttavia, nel tuo esempio specifico, l'esplicito IllegalArgumentExceptionnon fornisce un miglioramento significativo rispetto a un NullReferenceException- perché entrambi gli oggetti su cui vengono operati vengono già consegnati come argomenti alla funzione.

Ma diamo un'occhiata a un esempio leggermente diverso.

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

Se non fosse presente alcun argomento per il controllo nel costruttore, si otterrebbe un NullReferenceExceptionquando si chiama Calculate.

Ma il pezzo di codice rotto non era né la Calculatefunzione, né il consumatore della Calculatefunzione. Il pezzo di codice non funzionante era il codice che tenta di costruire il valore PersonCalculatornull Person, quindi è qui che vogliamo che si verifichi l'eccezione.

Se rimuoviamo tale controllo esplicito dell'argomento, dovrai capire perché si è NullReferenceExceptionverificato un errore quando è Calculatestato chiamato. E rintracciare perché l'oggetto è stato costruito con una nullpersona può diventare complicato, soprattutto se il codice che costruisce la calcolatrice non è vicino al codice che chiama effettivamente la Calculatefunzione.


0

Non negli esempi che dai.

Come dici tu, lanciare esplicitamente non ti sta guadagnando molto quando otterrai un'eccezione poco dopo. Molti sosterranno che avere l'eccezione esplicita con un buon messaggio è meglio, anche se non sono d'accordo. In scenari pre-rilasciati, la traccia dello stack è abbastanza buona. Negli scenari post-release, il sito di chiamata può spesso fornire messaggi migliori rispetto alla funzione.

Il secondo modulo fornisce troppe informazioni alla funzione. Tale funzione non dovrebbe necessariamente sapere che le altre funzioni genereranno un input nullo. Anche se lo fanno due su input nulla ora , diventa molto fastidioso per refactoring dovrebbe essere fermata che il caso, poiché il controllo di nulla è diffusa in tutto il codice.

Ma in generale, dovresti lanciare in anticipo una volta determinato che qualcosa è andato storto (nel rispetto del DRY). Questi però non sono forse grandi esempi di ciò.


-3

Nella tua funzione di esempio, preferirei che tu non abbia effettuato controlli e consenti NullReferenceExceptionche ciò accada.

Per uno, non ha senso passare un null lì, quindi capirò immediatamente il problema basandomi sul lancio di a NullReferenceException.

In secondo luogo, se ogni funzione genera eccezioni leggermente diverse in base al tipo di input ovviamente errato fornito, allora molto presto hai funzioni che potrebbero generare 18 diversi tipi di eccezioni e molto presto ti ritrovi a dire che è troppo molto lavoro da affrontare e solo sopprimere tutte le eccezioni.

Non c'è niente che tu possa davvero fare per aiutare la situazione di un errore di progettazione nella tua funzione, quindi lascia che l'errore si verifichi senza cambiarlo.


Lavoro da alcuni anni su una base di codice basata su questo principio: i puntatori vengono semplicemente de-referenziati quando necessario e quasi mai controllati contro NULL e gestiti in modo esplicito. Il debug di questo codice è sempre stato molto dispendioso in termini di tempo e di problemi poiché eccezioni (o core dump) si verificano molto più tardi nel punto in cui si trova l'errore logico. Ho sprecato letteralmente settimane alla ricerca di alcuni bug. Per questo motivo, preferisco lo stile fail-as-just-as-possible, almeno per il codice complesso in cui non è immediatamente ovvio da dove provenga un'eccezione puntatore null.
Giorgio,

"Per uno, non ha senso passare un null lì, quindi capirò immediatamente il problema basandomi sul lancio di una NullReferenceException.": Non necessariamente, come illustra il secondo esempio di Prog.
Giorgio,

"Due, è che se ogni funzione genera eccezioni leggermente diverse in base al tipo di input ovviamente errato fornito, allora molto presto hai funzioni che potrebbero generare 18 diversi tipi di eccezioni ...": In questo caso può usare la stessa eccezione (anche NullPointerException) in tutte le funzioni: l'importante è lanciare in anticipo, non lanciare un'eccezione diversa da ogni funzione.
Giorgio,

Ecco a cosa servono RuntimeExceptions. Qualsiasi condizione che "non dovrebbe accadere" ma impedisce al codice di funzionare dovrebbe generarne una. Non è necessario dichiararli nella firma del metodo. Ma è necessario catturarli ad un certo punto e segnalare che la funzione dell'azione richiesta non è riuscita.
Florian F,

1
La domanda non specifica una lingua, quindi affidarsi ad esempio RuntimeExceptionnon è necessariamente un presupposto valido.
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.