Genera eccezione o lascia che il codice fallisca


52

Mi chiedo se ci sono pro e contro contro questo stile:

private void LoadMaterial(string name)
{
    if (_Materials.ContainsKey(name))
    {
        throw new ArgumentException("The material named " + name + " has already been loaded.");
    }

    _Materials.Add(
        name,
        Resources.Load(string.Format("Materials/{0}", name)) as Material
    );
}

Tale metodo dovrebbe, per ciascuno name, essere eseguito una sola volta. _Materials.Add()sarà un'eccezione se sarà chiamato più volte per lo stesso name. La mia guardia, di conseguenza, è completamente ridondante o ci sono alcuni benefici meno ovvi?

Questo è C #, Unity, se qualcuno è interessato.


3
Cosa succede se si carica un materiale due volte senza la protezione? Genera _Materials.Addun'eccezione?
user253751

2
In realtà ho già menzionato nei lanci un'eccezione: P
asincrono il

9
Note a margine: 1) Considerare l'utilizzo della string.Formatconcatenazione di stringhe per la creazione del messaggio di eccezione. 2) utilizzare solo asse si prevede che il cast fallisca e controllare il risultato per null. Se ti aspetti di ottenere sempre un Material, usa un cast come (Material)Resources.Load(...). È più facile eseguire il debug di un'eccezione di cast chiara rispetto a un'eccezione di riferimento null che si verifica in seguito.
Codici A Caos il

3
In questo caso specifico, i chiamanti potrebbero anche trovare utile un compagno LoadMaterialIfNotLoadedo un ReloadMaterialmetodo (questo nome potrebbe utilizzare il miglioramento).
jpmc26,

1
In un'altra nota: questo modello è ok per l'aggiunta a volte di un elemento a un elenco. Tuttavia, non utilizzare questo modello per riempire un intero elenco perché si verificherà una situazione O (n ^ 2) in cui con elenchi di grandi dimensioni si verificheranno problemi di prestazioni. Non lo noterai a 100 voci ma a 1000 voci certamente e a 10.000 voci sarà un grosso collo di bottiglia.
Pieter B,

Risposte:


109

Il vantaggio è che la tua eccezione "personalizzata" ha un messaggio di errore che è significativo per chiunque chiami questa funzione senza sapere come è implementata (che in futuro potresti essere tu!).

Certo, in questo caso sarebbero probabilmente in grado di indovinare cosa significasse l'eccezione "standard", ma stai ancora chiarendo che hanno violato il tuo contratto, piuttosto che imbatterti in qualche strano bug nel tuo codice.


3
Essere d'accordo. È quasi sempre meglio lanciare un'eccezione per una violazione del presupposto.
Frank Hileman,

22
"Il vantaggio è che la tua eccezione" personalizzata "ha un messaggio di errore che è significativo per chiunque chiami questa funzione senza sapere come è implementata (che in futuro potresti essere tu!)." I migliori consigli.
asincrono il

2
"Esplicito è meglio che implicito." - Zen of Python
jpmc26

4
Anche se l'errore generato _Materials.Addnon è l'errore che si desidera trasmettere al chiamante, penso che questo metodo di rietichettatura dell'errore sia inefficiente. Per ogni chiamata riuscita di LoadMaterial(funzionamento normale) avrai fatto lo stesso test di sanità mentale due volte , dentro LoadMateriale poi di nuovo dentro _Materials.Add. Se vengono avvolti più livelli, ognuno con lo stesso stile, è possibile avere più volte lo stesso test.
Marc van Leeuwen,

15
... Invece considera di immergerti _Materials.Addincondizionatamente, quindi cogli un potenziale errore e nel gestore lancia un altro. Il lavoro extra ora viene svolto solo in casi errati, l'eccezionale percorso di esecuzione in cui non ti preoccupi affatto dell'efficienza.
Marc van Leeuwen,

100

Sono d'accordo con la risposta di Ixrec . Tuttavia, potresti voler considerare una terza alternativa: rendere idempotente la funzione. In altre parole, torna presto invece di lanciare un ArgumentException. Questo è spesso preferibile se si sarebbe altrimenti costretti a verificare se è già stato caricato prima di chiamare LoadMaterialogni volta. Meno precondizioni hai, meno carico cognitivo sul programmatore.

Generare un'eccezione sarebbe preferibile se si tratta davvero di un errore del programmatore, dove dovrebbe essere ovvio e noto al momento della compilazione se il materiale è già stato caricato, senza dover controllare in fase di esecuzione prima di chiamare.


2
D'altra parte, avere una funzione che fallisce silenziosamente può causare comportamenti inaspettati con conseguente difficoltà a trovare bug in seguito.
Philipp

30
@Philipp la funzione non fallirebbe in silenzio. Essere idempotenti significa che eseguirlo molte volte ha lo stesso effetto di eseguirlo una volta. In altre parole, se il materiale è già caricato, le nostre specifiche ("il materiale deve essere caricato") sono già soddisfatte, quindi non dobbiamo fare nulla. Possiamo solo tornare.
Warbo,

8
Mi piace di più, in realtà. Naturalmente, a seconda dei vincoli dati dai requisiti delle applicazioni, questo potrebbe essere sbagliato. Poi di nuovo, sono un fan assoluto dei metodi idempotenti.
FP

6
@Philipp se ci pensiamo LoadMaterialha i seguenti vincoli: "Dopo averlo chiamato il materiale viene sempre caricato e il numero di materiali caricati non è diminuito". Generare un'eccezione per un terzo vincolo "Il materiale non può essere aggiunto due volte" mi sembra sciocco e controintuitivo. Rendere idempotente la funzione riduce la dipendenza del codice dall'ordine di esecuzione e dallo stato dell'applicazione. Sembra una doppia vittoria per me.
Benjamin Gruenbaum

7
Mi piace questa idea, ma suggerisco un piccolo cambiamento: restituire un valore booleano che rifletta se qualcosa è stato caricato. Se il chiamante si preoccupa davvero della logica dell'applicazione, può verificarlo e generare un'eccezione.
user949300,

8

La domanda fondamentale che devi porre è: quale vuoi che sia l'interfaccia per la tua funzione? In particolare, la condizione è _Materials.ContainsKey(name)una condizione preliminare della funzione?

Se è non precondizione, la funzione deve fornire un risultato ben definito per tutte le possibili valli di name. In questo caso, l'eccezione generata se namenon _Materialsfa parte diventa parte dell'interfaccia della funzione. Ciò significa che deve far parte della documentazione dell'interfaccia e se deciderai di cambiare quell'eccezione in futuro, si tratterà di una rottura dell'interfaccia .

La domanda più interessante è cosa succede se si tratta di una condizione preliminare. In tal caso, la condizione preliminare stessa diventa parte dell'interfaccia della funzione, ma il modo in cui una funzione si comporta quando viene violata questa condizione non fa necessariamente parte dell'interfaccia.

In questo caso, l'approccio che hai pubblicato, verificando le violazioni delle condizioni preliminari e segnalando l'errore, è ciò che è noto come programmazione difensiva . La programmazione difensiva è buona in quanto avvisa l'utente in anticipo quando ha fatto un errore e ha chiamato la funzione con un argomento falso. È negativo in quanto aumenta significativamente l'onere della manutenzione, poiché il codice utente potrebbe dipendere dal fatto che la funzione gestisce le violazioni delle condizioni preliminari in un certo modo. In particolare, se in futuro il controllo di runtime diventa un collo di bottiglia delle prestazioni (improbabile per un caso così semplice, ma abbastanza comune per precondizioni più complesse), potrebbe non essere più possibile rimuoverlo.

Si scopre che quegli svantaggi possono essere molto significativi, il che ha dato una sorta di cattiva reputazione alla programmazione difensiva in alcuni ambienti. Tuttavia, l'obiettivo iniziale è ancora valido: vogliamo che gli utenti della nostra funzione notino presto quando hanno fatto un errore.

Pertanto molti sviluppatori al giorno d'oggi propongono un approccio leggermente diverso per questo tipo di problemi. Invece di lanciare un'eccezione, usano un meccanismo assertivo per controllare i presupposti. In altre parole, i prerequisiti possono essere controllati nelle build di debug per aiutare gli utenti a rilevare tempestivamente gli errori, ma non fanno parte dell'interfaccia delle funzioni. La differenza può sembrare sottile a prima vista, ma può fare un'enorme differenza nella pratica.

Tecnicamente, chiamare una funzione con precondizioni violate è un comportamento indefinito. Ma l'implementazione potrebbe decidere di rilevare quei casi e avvisare immediatamente l'utente se ciò accade. Sfortunatamente le eccezioni non sono un buon strumento per implementarlo, poiché il codice utente può reagire su di loro e quindi potrebbe iniziare a fare affidamento sulla loro presenza.

Per una spiegazione dettagliata dei problemi con il classico approccio difensivo, nonché una possibile implementazione per controlli precondizionati in stile assertivo, fare riferimento al discorso sulla programmazione difensiva di John Lakos, fatto proprio da CppCon 2014 ( Slides , Video ).


4

Ci sono già alcune risposte qui, ma ecco la risposta che tiene conto di Unity3D (la risposta è molto specifica per Unity3D, farei la maggior parte in modo molto diverso nella maggior parte dei contesti):

In generale, Unity3D non utilizza le eccezioni tradizionalmente. Se si genera un'eccezione in Unity3D, non è come nell'applicazione .NET media, vale a dire che non fermerà il programma, la maggior parte è possibile configurare l'editor per mettere in pausa. Sarà appena registrato. Questo può facilmente mettere il gioco in uno stato non valido e creare un effetto a cascata che rende difficile rintracciare gli errori. Quindi direi che nel caso di Unity, lasciare Addun'eccezione è un'opzione particolarmente indesiderabile.

Ma sull'esame della velocità delle eccezioni non è un caso di ottimizzazione prematura in alcuni casi a causa del modo in cui Mono funziona su Unity su alcune piattaforme. In effetti, Unity3D su iOS supporta alcune ottimizzazioni di script avanzate e le eccezioni disabilitate * sono un effetto collaterale di una di esse. Questo è davvero qualcosa da considerare perché queste ottimizzazioni non si sono rivelate molto utili per molti utenti, mostrando un caso realistico per considerare la possibilità di limitare l'uso delle eccezioni in Unity3D. (* eccezioni gestite dal motore, non dal codice)

Direi che in Unity potresti voler adottare un approccio più specializzato. Ironia della sorte, una risposta molto votata al momento della stesura di questo , mostra un modo in cui potrei implementare qualcosa di simile specificamente nel contesto di Unity3D (altrove qualcosa di simile è davvero inaccettabile, e anche in Unity è abbastanza inelegante).

Un altro approccio che prenderei in considerazione non è in realtà indicare un errore per quanto riguarda il chiamante, ma piuttosto usare le Debug.LogXXfunzioni. In questo modo ottieni lo stesso comportamento del lancio di un'eccezione non gestita (a causa di come Unity3D li gestisce) senza rischiare di mettere qualcosa in uno stato strano lungo la linea. Considera anche se questo è davvero un errore (Sta cercando di caricare lo stesso materiale due volte necessariamente un errore nel tuo caso? O potrebbe essere un caso in cui Debug.LogWarningè più applicabile).

E per quanto riguarda l'uso di cose come le Debug.LogXXfunzioni anziché le eccezioni, devi ancora considerare cosa succede quando un'eccezione verrebbe generata da qualcosa che restituisce un valore (come GetMaterial). Tendo ad avvicinarmi a questo passando null insieme a registrare l'errore (di nuovo, solo in Unity ). Quindi uso i controlli null nei miei MonoBehaviors per assicurarmi che qualsiasi dipendenza come un materiale non sia un valore null e disabilito il MonoBehavior se lo è. Un esempio per un comportamento semplice che richiede alcune dipendenze è qualcosa del genere:

    public void Awake()
    {
        _inputParameters = GetComponent<VehicleInputParameters>();
        _rigidbody = GetComponent<Rigidbody>();
        _rigidbodyTransform = _rigidbody.transform;
        _raycastStrategySelector = GetComponent<RaycastStrategySelectionBehavior>();

        _trackParameters =
            SceneManager.InstanceOf.CurrentSceneData.GetValue<TrackParameters>();

        this.DisableIfNull(() => _rigidbody);
        this.DisableIfNull(() => _raycastStrategySelector);
        this.DisableIfNull(() => _inputParameters);
        this.DisableIfNull(() => _trackParameters);
    }

SceneData.GetValue<>è simile al tuo esempio in quanto chiama una funzione su un dizionario che genera un'eccezione. Invece di generare un'eccezione, utilizza Debug.LogErroruna traccia dello stack come farebbe un'eccezione normale e restituisce null. I controlli che seguono * disabiliteranno il comportamento invece di lasciarlo continuare a esistere in uno stato non valido.

* i controlli sembrano così a causa di un piccolo aiuto che uso per stampare un messaggio formattato quando disabilita l'oggetto di gioco **. Semplici controlli null con iflavoro qui (** i controlli dell'helper sono compilati solo nelle build di debug (come assert). L'uso di lambda ed espressioni come quelle in Unity può influire negativamente sulle prestazioni)


1
+1 per l'aggiunta di alcune informazioni veramente nuove alla pila. Non me l'aspettavo.
Ixrec,

3

Mi piacciono le due risposte principali, ma voglio suggerire che i nomi delle tue funzioni potrebbero essere migliorati. Sono abituato a Java, quindi YMMV, ma un metodo "add" dovrebbe, IMO, non generare un'eccezione se l'elemento è già lì. Dovrebbe aggiungere nuovamente l'elemento o, se la destinazione è un set, non fare nulla. Poiché questo non è il comportamento di Materials.Add, dovrebbe essere rinominato TryPut o AddOnce o AddOrThrow o simili.

Allo stesso modo, LoadMaterial dovrebbe essere rinominato LoadIfAbsent o Put o TryLoad o LoadOrThrow (a seconda che si usi la risposta n. 1 o n. 2) o alcuni di questi.

Segui le convenzioni di denominazione di C # Unity qualunque esse siano.

Ciò sarà particolarmente utile se si dispone di altre funzioni AddFoo e LoadBar in cui è possibile caricare la stessa cosa due volte. Senza nomi chiari gli sviluppatori saranno frustrati.


C'è una differenza tra la semantica di C # Dictionary.Add () (vedi msdn.microsoft.com/en-us/library/k7z0zy8k%28v=vs.110%29.aspx ) e la semantica di Java Collection.add () (vedi documenti. oracle.com/javase/8/docs/api/java/util/… ). C # restituisce null e genera un'eccezione. Considerando che Java restituisce bool e sostituisce il valore precedentemente memorizzato con il nuovo valore o genera un'eccezione quando il nuovo elemento non può essere aggiunto.
Kasper van den Berg,

Kasper - grazie e interessante. Come si sostituisce un valore in un dizionario C #? (Equivalente a un put Java ()). Non vedi un metodo integrato in quella pagina.
user949300

buona domanda, si potrebbe fare Dictionary.Remove () (consultare msdn.microsoft.com/en-us/library/bb356469%28v=vs.110%29.aspx ) seguito da un Dictionary.Add () (vedere msdn.microsoft .com / it-it / library / bb338565% 28v = vs.110% 29.aspx ), ma questo sembra un po 'imbarazzante.
Kasper van den Berg,

@ user949300 dizionario [chiave] = valore sostituisce la voce se esiste già
Rupe

@Rupe e presumibilmente lancia qualcosa se non lo fa. Tutto mi sembra molto imbarazzante. La maggior parte dei clienti che istituiscono il dict non interessa wheteher una voce era lì, hanno appena cura che, dopo la loro codice di installazione, che è lì. Punteggio uno per l'API Java Collections (IMHO). PHP è anche imbarazzante con i dicts, è stato un errore per C # seguire quel precedente.
user949300,

3

Tutte le risposte aggiungono idee preziose, vorrei combinarle:

Decidi la semantica prevista e prevista LoadMaterial()dell'operazione. Esistono almeno le seguenti opzioni:

  • Presupposto per nameLoadedMaterials : →

    Quando viene violata la condizione preliminare, l'effetto di LoadMaterial()non è specificato (come nella risposta di ComicSansMS ). Ciò consente la massima libertà nell'attuazione e futuri cambiamenti di LoadMaterial(). O,

  • sono specificati gli effetti della chiamata LoadMaterial(name)con nameLoadedMaterials ; o:

    • la specifica afferma che viene generata un'eccezione; o
    • il disciplinare afferma che il risultato è idempotente (come nella risposta di Karl Bielefeldt ),

Dopo aver deciso la semantica, è necessario scegliere un'implementazione. Sono state proposte le seguenti opzioni e considerazioni:

  • Lancia un'eccezione personalizzata (come suggerito da Ixrec ) →

    Il vantaggio è che la tua eccezione "personalizzata" presenta un messaggio di errore significativo per chiunque chiami questa funzione - Ixrec e User16547

    • Per evitare il costo del controllo ripetuto nameLoadedMaterials è possibile seguire i consigli di Marc van Leeuwen :

      ... Invece considera di immergerti in _Materials.Aggiungi incondizionatamente, quindi rileva un potenziale errore e nel gestore lancia un altro.

  • Lascia che Dictionay.Add lanci l'eccezione →

    Il codice di lancio dell'eccezione è ridondante. - Jon Raynor

    Tuttavia , la maggior parte degli elettori è più d'accordo con Ixrec

    Ulteriori motivi per scegliere questa implementazione sono:

    • che i chiamanti possono già gestire le ArgumentExceptioneccezioni e
    • per evitare di perdere informazioni sullo stack.

    Ma se questi due motivi sono importanti, puoi anche derivare la tua eccezione personalizzata ArgumentExceptione utilizzare l'originale come eccezione concatenata.

  • Fai LoadMaterial()idempotente come anwser da Karl Bielefeldt e upvoted il più delle volte (75 volte).

    Le opzioni di implementazione per questo comportamento:

    • Verifica con Dictionary.ContainsKey ()

    • Chiamare sempre Dictionary.Add () catturare il ArgumentExceptionlancio quando la chiave da inserire esiste già e ignorare tale eccezione. Documenta che ignorare l'eccezione è ciò che intendi fare e perché.

      • Quando LoadMaterials()viene (quasi) sempre chiamato una volta per ciascuna name, questo evita il costo ripetutamente controllando nameLoadedMaterials cf. Marc van Leeuwen . Però,
      • quando LoadedMaterials()viene spesso chiamato più volte per lo stesso name, ciò comporta il costo (costoso) di lanciare ArgumentExceptione impilare lo svolgimento.
    • Ho pensato che esistesse un TryAddanalogo -metodo a TryGet () che avrebbe permesso di evitare il dispendioso lancio di eccezioni e di impilare lo svolgimento di chiamate non riuscite a Dictionary.Add.

      Ma questo TryAddmetodo sembra non esistere.


1
+1 per essere la risposta più completa. Ciò probabilmente va ben oltre ciò che l'OP era originariamente dopo, ma è un utile sommario di tutte le opzioni.
Ixrec,

1

Il codice di lancio dell'eccezione è ridondante.

Se chiami:

_Materials.Add (name, Resources.Load (string.Format ("Materials / {0}", name)) come materiale

con la stessa chiave lancerà a

System.ArgumentException

Il messaggio sarà: "Un elemento con la stessa chiave è già stato aggiunto."

Il ContainsKeycontrollo è ridondante poiché essenzialmente lo stesso comportamento si ottiene senza di esso. L'unico elemento diverso è il messaggio effettivo nell'eccezione. Se c'è un vero vantaggio dall'avere un messaggio di errore personalizzato, allora il codice di protezione ha qualche merito.

Altrimenti, probabilmente rifatterei la guardia in questo caso.


0

Penso che questa sia più una domanda sulla progettazione delle API che una domanda sulla convenzione di codifica.

Qual è il risultato atteso (il contratto) di chiamare:

LoadMaterial("wood");

Se il chiamante può aspettarsi / è garantito che dopo aver chiamato questo metodo, il materiale "legno" viene caricato, allora non vedo alcun motivo per lanciare un'eccezione quando quel materiale è già caricato.

Se si verifica un errore durante il caricamento del materiale, ad esempio, non è stato possibile aprire la connessione al database o non è presente materiale "legno" nel repository, è corretto generare un'eccezione per avvisare il chiamante del problema.


-2

Perché non cambiare il metodo in modo che ritorni "vero" se viene aggiunto il materiale e "falso" se non lo fosse?

private bool LoadMaterial(string name)
{
   if (_Materials.ContainsKey(name))

    {

        return false; //already present
    }

    _Materials.Add(
        name,
        Resources.Load(string.Format("Materials/{0}", name)) as Material
    );

    return true;

}

15
Le funzioni che restituiscono valori di errore sono esattamente l'anti-pattern che le eccezioni dovrebbero rendere obsolete.
Philipp,

È sempre così? Ho pensato che fosse solo il caso di semipredicate ( en.wikipedia.org/wiki/Semipredicate_problem ) Perché nell'esempio di codice OP, non riuscire ad aggiungere un materiale al sistema non è un vero problema. Il metodo lo scopo per assicurarsi che il materiale sia nel sistema quando lo si esegue, ed è esattamente ciò che fa questo metodo. (anche se fallisce) Il valore booleano di ritorno indica semplicemente se il metodo ha già tentato di aggiungere o meno questo argomento. ps: non prendo in considerazione che potrebbero esserci più materiali con lo stesso nome.
MrIveck,

1
Penso di aver trovato la soluzione da solo: en.wikipedia.org/wiki/… Questo sottolinea che la risposta di Ixrec è la più corretta
MrIveck,

@Philipp Nel caso il programmatore / le specifiche abbiano deciso che non si verificano errori quando si tenta di caricare due volte. Il valore restituito non è, in tal senso, un valore di errore ma informativo.
user949300,
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.