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

29

Uso le eccezioni per individuare i problemi in anticipo. Ad 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 null in questa funzione. Non ho mai intenzione di farlo. Tuttavia, come tutti sappiamo, le cose non intenzionali si verificano nella programmazione.

Lanciare un'eccezione se questo problema si verifica, mi consente di individuarlo e correggerlo, prima che causi più problemi in altri punti del programma. L'eccezione arresta il programma e mi dice "sono successe cose brutte qui, risolvilo". Invece di questo null si sposta il programma causando problemi altrove.

Ora, hai ragione, in questo caso null causerebbe semplicemente un NullPointerException subito, quindi potrebbe non essere l'esempio migliore.

Ma considera un metodo come questo, ad esempio:

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

In questo caso, un null come parametro passerebbe attorno al programma e potrebbe causare errori molto più tardi, il che sarà difficile risalire all'origine.

Modifica della 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 consente di individuare il problema molto prima che causi strani errori in altri luoghi.

Inoltre, un riferimento null come parametro è solo un esempio. Potrebbero essere molti tipi di problemi, da argomenti non validi a qualsiasi altra cosa. È sempre meglio individuarli prima.

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

    
posta Aviv Cohn 14.09.2014 - 19:05
fonte

7 risposte

29

Sì, "fallire presto" è un ottimo principio, e questo è semplicemente un modo possibile per implementarlo. E nei metodi che devono restituire un valore specifico, non c'è davvero molto altro che può fare per fallire deliberatamente - si tratta di generare eccezioni o innescare asserzioni. Le eccezioni dovrebbero segnalare condizioni "eccezionali" e il rilevamento di un errore di programmazione è certamente eccezionale.

    
risposta data 14.09.2014 - 19:29
fonte
8

Sì, lanciare eccezioni è una buona idea. Lanciali presto, gettali spesso, gettali con entusiasmo.

So che esiste un dibattito "eccezioni contro asserzioni", con alcuni tipi di comportamento eccezionale (in particolare, quelli pensati per riflettere errori di programmazione) gestiti da asserzioni che possono essere "compilate" per il runtime anziché eseguire il debug / test delle build . Ma la quantità di prestazioni consumate in alcuni controlli di correttezza extra è minima per l'hardware moderno, e qualsiasi costo aggiuntivo è di gran lunga superato dal valore di avere risultati corretti e non corretti. Non ho mai effettivamente incontrato una base di codice di applicazione per la quale vorrei (la maggior parte) verifiche rimosse in fase di runtime.

Sono tentato di dire che non vorrei un sacco di controlli extra e condizionali all'interno degli stretti cicli di codice numericamente intensivo ... ma in realtà sono generati molti errori numerici, e se non ci sono si propagano verso l'esterno per effettuare tutti i risultati. Quindi, anche lì, vale la pena fare un controllo. In effetti, alcuni degli algoritmi numerici migliori e più efficienti si basano sulla valutazione degli errori.

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

Un miglioramento che ho trovato è quello di non lanciare solo eccezioni di livello base. IllegalArgumentException è buono, ma può provenire essenzialmente da qualsiasi luogo. Non ci vuole molto in molte lingue per aggiungere eccezioni personalizzate. Per il tuo modulo di gestione personale, dì:

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 le entità inutilmente (il Rasoio di Occam). Spesso bastano poche eccezioni personalizzate per segnalare "questo modulo non sta ottenendo i dati corretti!" o "questo modulo non può fare ciò che deve fare!" in un modo specifico e su misura, ma non così preciso da dover implementare nuovamente l'intera gerarchia delle eccezioni. Spesso vengo a quella piccola serie di eccezioni personalizzate iniziando con le eccezioni di magazzino, quindi eseguendo la scansione del codice e rendendosi conto che "questi N posti stanno sollevando eccezioni di magazzino, ma si riducono all'idea di livello superiore che non ottengono i dati hanno bisogno: sostituiamo le eccezioni di magazzino con un'eccezione di livello superiore che comunica in modo più chiaro cosa sta realmente accadendo. "

    
risposta data 15.09.2014 - 21:24
fonte
3

Fallire al più presto è grandioso quando si esegue il debug di un'applicazione. Ricordo un particolare errore di segmentazione in un programma C ++ legacy: il luogo in cui è stato rilevato il bug non aveva nulla a che fare con il luogo in cui è stato introdotto (il puntatore nullo è stato felicemente spostato da un posto all'altro in memoria prima che causasse infine 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, specialmente con riferimenti null.

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

Per un esempio più realistico, puoi utilizzare le dichiarazioni di assert , che:

  1. Sono più brevi da scrivere e leggere:

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

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

Quindi, esponendo le assunzioni circa gli input e / o lo stato della tua applicazione con asserzioni, lascia che il prossimo sviluppatore capisca un po 'di più lo scopo del tuo codice.

Un analizzatore statico (ad esempio il compilatore) potrebbe essere anche 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.

    
risposta data 15.09.2014 - 10:01
fonte
1

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

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

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, cioè quando viene chiamato registerPerson() , non quando un'eccezione di puntatore nullo viene lanciata da qualche parte nello stack di chiamate. Il debugging diventa molto più semplice: sappiamo tutti fino a che punto un valore non valido può attraversare il codice prima che si manifesti come un bug.
  2. Riduce l'accoppiamento tra le funzioni: registerPerson() non formula alcuna ipotesi su quali altre funzioni finiranno con l'argomento person e su come lo useranno: la decisione che null è un errore viene presa e implementata a livello locale.

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

    
risposta data 14.09.2014 - 22:32
fonte
1

In generale, sì, è una buona idea "fallire presto". Tuttavia, nel tuo esempio specifico, il IllegalArgumentException esplicito non fornisce un miglioramento significativo su un NullReferenceException - poiché entrambi gli oggetti utilizzati vengono consegnati come argomenti alla funzione già.

Ma guardiamo 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 ci sono stati controlli degli argomenti nel costruttore, si otterrebbe un NullReferenceException quando si chiama Calculate .

Ma il frammento di codice non era né la funzione Calculate , né il consumatore della funzione Calculate . Il pezzo di codice spezzato era il codice che tenta di costruire PersonCalculator con un Person nullo, quindi è qui che vogliamo che si verifichi l'eccezione.

Se rimuoviamo il controllo esplicito degli argomenti, dovrai capire perché si è verificato un NullReferenceException quando è stato chiamato Calculate . E rintracciare perché l'oggetto è stato costruito con una persona null può diventare complicato, specialmente se il codice che costruisce la calcolatrice non è vicino al codice che in realtà chiama la funzione Calculate .

    
risposta data 15.09.2014 - 12:45
fonte
0

Non negli esempi che dai.

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

Il secondo modulo fornisce troppe informazioni alla funzione. Quella funzione non dovrebbe necessariamente sapere che le altre funzioni genereranno input nulli. Anche se lanciano su input nulli ora , diventa molto problematico per il refactator che dovrebbe smettere di essere il caso dal momento che il controllo nullo è diffuso in tutto il codice.

Ma in generale, dovresti lanciare presto una volta stabilito che qualcosa è andato storto (pur rispettando DRY). Questi però non sono grandi esempi di questo.

    
risposta data 14.09.2014 - 19:36
fonte
-3

Nella tua funzione di esempio, preferirei che non avessi controllato e permetti semplicemente che accada NullReferenceException .

Per uno, non ha senso passare un null lì comunque, quindi vedrò subito il problema sulla base del lancio di un NullReferenceException .

Due, è che se ogni funzione lancia eccezioni leggermente diverse in base al tipo di input ovviamente errati, allora molto presto avrai funzioni che potrebbero generare 18 tipi diversi di eccezioni, e molto presto ti ritrovi dire che è troppo lavoro da affrontare e comunque sopprimere tutte le eccezioni.

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

    
risposta data 15.09.2014 - 02:42
fonte

Leggi altre domande sui tag