Come già mostrano i tuoi esempi, ognuno di questi casi deve essere valutato separatamente e c'è un considerevole spettro di grigio tra "circostanze eccezionali" e "controllo del flusso", specialmente se il tuo metodo è destinato a essere riutilizzabile e potrebbe essere usato in schemi abbastanza diversi di quanto fosse originariamente progettato per. Non aspettatevi che tutti qui concordiamo sul significato di "non eccezionale", soprattutto se discutete immediatamente della possibilità di utilizzare "eccezioni" per implementarlo.
Potremmo anche non essere d'accordo su quale progetto renda il codice più facile da leggere e mantenere, ma presumo che il progettista della biblioteca abbia una chiara visione personale di ciò e debba solo bilanciarlo con le altre considerazioni coinvolte.
Risposta breve
Segui le tue sensazioni visive tranne quando stai progettando metodi abbastanza veloci e ti aspetti una possibilità di riutilizzo imprevisto.
Risposta lunga
Ogni chiamante futuro può tradurre liberamente tra codici di errore ed eccezioni come desidera in entrambe le direzioni; questo rende i due approcci di progettazione quasi equivalenti, tranne per le prestazioni, la facilità di debug e alcuni contesti di interoperabilità limitati. Questo di solito si riduce alle prestazioni, quindi concentriamoci su quello.
Come regola generale, aspettati che lanciare un'eccezione sia 200 volte più lento di un ritorno normale (in realtà, c'è una varianza significativa in questo).
Come altra regola empirica, lanciare un'eccezione può spesso consentire un codice molto più pulito rispetto al più grezzo dei valori magici, perché non si fa affidamento sul programmatore che traduce il codice di errore in un altro codice di errore, poiché viaggia attraverso più livelli di codice client verso un punto in cui esiste un contesto sufficiente per gestirlo in modo coerente e adeguato. (Caso speciale: null
tende ad andare meglio qui rispetto ad altri valori magici a causa della sua tendenza a tradursi automaticamente in un NullReferenceException
caso di alcuni, ma non tutti i tipi di difetti; di solito, ma non sempre, abbastanza vicino alla fonte del difetto. )
Quindi qual è la lezione?
Per una funzione chiamata solo poche volte durante la vita di un'applicazione (come l'inizializzazione dell'app), usa tutto ciò che ti offre un codice più pulito e più comprensibile. Le prestazioni non possono essere motivo di preoccupazione.
Per una funzione usa e getta, usa tutto ciò che ti dà un codice più pulito. Quindi eseguire un po 'di profilazione (se necessario) e modificare le eccezioni ai codici di ritorno se rientrano tra i principali colli di bottiglia in base alle misurazioni o alla struttura generale del programma.
Per una costosa funzione riutilizzabile, usa tutto ciò che ti dà un codice più pulito. Se in pratica devi sempre subire un roundtrip di rete o analizzare un file XML su disco, il sovraccarico di generare un'eccezione è probabilmente trascurabile. È più importante non perdere i dettagli di eventuali guasti, nemmeno accidentalmente, piuttosto che tornare da un "guasto non eccezionale" in più tempo.
Una funzione riutilizzabile snella richiede più pensiero. Impiegando eccezioni, si sta forzando qualcosa come un rallentamento 100 volte sui chiamanti che vedranno l'eccezione su metà delle (molte) chiamate, se il corpo della funzione viene eseguito molto velocemente. Le eccezioni sono ancora un'opzione di progettazione, ma dovrai fornire un'alternativa bassa ai chiamanti che non possono permetterselo. Diamo un'occhiata a un esempio.
Elencate un ottimo esempio di Dictionary<,>.Item
, che, in termini vaghi, è cambiato dal ritorno dei null
valori al lancio KeyNotFoundException
tra .NET 1.1 e .NET 2.0 (solo se siete disposti a considerarlo il Hashtable.Item
suo pratico precursore non generico). La ragione di questo "cambiamento" non è senza interesse qui. L'ottimizzazione delle prestazioni dei tipi di valore (non più boxe) ha reso il valore magico originale ( null
) una non opzione; out
i parametri riporterebbero solo una piccola parte del costo delle prestazioni. Quest'ultima considerazione sulle prestazioni è completamente trascurabile rispetto al sovraccarico del lancio di un KeyNotFoundException
, ma il design dell'eccezione è ancora superiore qui. Perché?
- i parametri di ref / out comportano costi ogni volta, non solo nel caso di "fallimento"
- Chiunque si preoccupi può chiamare
Contains
prima di qualsiasi chiamata all'indicizzatore e questo schema è completamente naturale. Se uno sviluppatore vuole ma dimentica di chiamare Contains
, non possono insinuarsi problemi di prestazioni; KeyNotFoundException
è abbastanza forte da essere notato e riparato.