Lanciare un'eccezione quando appropriato fa parte del lavoro di un costruttore.
Consideriamo perché abbiamo costruttori.
Una parte è utile per avere un metodo che imposta varie proprietà e forse esegue un lavoro di inizializzazione più avanzato (ad esempio, un FileStream
effettivamente accederà al file pertinente). Ma se fosse davvero così conveniente tutto il tempo, a volte non troveremmo conveniente l'inizializzatore dei membri.
Il motivo principale per i costruttori è che possiamo mantenere invarianti di oggetti.
Un oggetto invariante è qualcosa che possiamo dire su un oggetto all'inizio e alla fine di ogni chiamata di metodo. (Se è stato progettato per l'uso simultaneo avremmo anche invarianti che si sono tenuti durante le chiamate di metodo).
Una delle invarianti della classe Uri
è che se IsAbsoluteUri
è vero, allora Host
sarà stringa che è un host valido, (se IsAbsoluteUri
è falso Host
potrebbe essere un host valido se è schema-parente, o accedervi potrebbe causare un InvalidOperationException
).
Come tale quando utilizzo un oggetto di tale classe e ho controllato IsAbsoluteUri
So che posso accedere a Host
senza eccezioni. So anche che sarà effettivamente un nome host, piuttosto che ad es. un breve trattato sulle credenze medioevali e dei primi tempi nelle qualità apotropaiche dei bezoari.
Ok, quindi un po 'di codice di mettere un tale trattato in non è esattamente probabile, ma il codice mettendo alcuni sorta di spazzatura in un oggetto è certamente.
Il mantenimento di un invariante si riduce a fare in modo che le combinazioni di valori che l'oggetto detiene abbia sempre senso. Questo deve essere fatto in qualsiasi setter o metodo che muta l'oggetto (o rendendo l'oggetto immutabile, perché non si può mai avere una modifica non valida se non si ha mai una modifica) e quelli che impostano i valori inizialmente, vale a dire in un costruttore.
In un linguaggio fortemente tipizzato abbiamo un po 'di controllo da quel tipo di sicurezza (un numero che deve essere compreso tra 0
e 15
non sarà mai impostato "Modern analysis has found that bezoars do indeed neutralise arsenic."
perché il compilatore semplicemente non ti consente di farlo.), Ma e il resto?
Considerare i costruttori per List<T>
che accettano argomenti. Uno di questi prende un numero intero e imposta di conseguenza la capacità interna e l'altro uno IEnumerable<T>
di cui l'elenco è pieno. L'avvio di questi due costruttori sono:
public List(int capacity)
{
if (capacity < 0) throw new ArgumentOutOfRangeException("capacity", capacity, SR.ArgumentOutOfRange_NeedNonNegNum);
/* … */
public List(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException("collection");
Quindi, se si chiama new List<string>(-2)
o new List<int>(null)
si ottiene un'eccezione, piuttosto che una lista non valido.
Una cosa interessante da notare su questo caso, è che questo è un caso in cui potrebbero avere "risolto" le cose per il chiamante. In questo caso sarebbe stato sicuro considerare i numeri negativi come lo 0
e gli enumerabili null come quelli di uno vuoto. Hanno deciso di buttare comunque. Perché?
Bene, abbiamo tre casi da considerare quando si scrivono i costruttori (e in effetti altri metodi).
- Il chiamante fornisce valori che è possibile utilizzare direttamente.
Eh, usali.
- Il chiamante fornisce valori che non è possibile utilizzare in modo significativo. (ad esempio impostando un valore da un enum a un valore non definito).
Definire senz'altro un'eccezione.
- Il chiamante ci dà valori che possiamo massaggiare in valori utili. (ad esempio limitando un numero di risultati a un numero negativo, che noi potremmo considerare come uguale a zero allo).
Questo è il caso più complicato. Dobbiamo considerare:
Il significato è univoco? Se c'è più di un modo per considerare cosa significa "veramente", allora lancia un'eccezione.
È probabile che il chiamante sia arrivato a questo risultato in modo ragionevole? Se il valore è semplicemente stupido, allora il chiamante ha presumibilmente commesso un errore nel passarlo al costruttore (o al metodo) e non stai facendo loro alcun favore nel nascondere i propri errori. Per prima cosa, molto probabilmente fanno altri errori in altre chiamate, ma questo è il caso in cui diventa ovvio.
In caso di dubbio, lanciare un'eccezione. Per prima cosa, se sei in dubbio su cosa dovresti fare, è probabile che il chiamante sia in dubbio su ciò che dovrebbe aspettarsi che tu faccia. Per un altro è molto meglio tornare più tardi e girare il codice che getta nel codice che non trasforma il codice che non getta nel codice che lo fa, perché quest'ultimo è più probabile che trasformi gli usi di lavoro in applicazioni rotte.
Finora ho visto solo il codice che può essere considerato valido; ci è stato chiesto di fare qualcosa di stupido e ci siamo rifiutati. Un altro caso è quando ci è stato chiesto di fare qualcosa di ragionevole (o sciocco, ma non siamo riusciti a rilevarlo) e non siamo stati in grado di farlo. Prendere in considerazione:
Non c'è nulla di invalido in questa chiamata che dovrebbe assolutamente fallire. Tutti i controlli di validazione dovrebbero passare. Speriamo che apriamo il file a in modalità lettura e ci danno un oggetto FileStream
attraverso il quale possiamo accedervi.
Ma cosa succede se non c'è ? Oppure no D:\
(equivale alla stessa cosa, ma il codice interno potrebbe non riuscire in un modo diverso) o non abbiamo il permesso di aprirlo. Oppure è bloccato da un altro processo?
In tutti questi casi non riusciamo a fare ciò che viene richiesto. Non va bene il nostro ritorno di un oggetto che rappresenta i tentativi di leggere un file che falliranno tutti! Quindi, di nuovo, facciamo un'eccezione qui.
OK. Ora considera il caso di StreamReader()
che prende un percorso. Funziona un po 'come (regolazione per tagliare fuori alcuni indirezione per il bene di esempio):
public StreamReader(String path, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize)
{
if (path==null || encoding==null)
throw new ArgumentNullException((path==null ? "path" : "encoding"));
if (path.Length==0)
throw new ArgumentException(Environment.GetResourceString("Argument_EmptyPath"));
if (bufferSize <= 0)
throw new ArgumentOutOfRangeException("bufferSize", Environment.GetResourceString("ArgumentOutOfRange_NeedPosNum"));
Contract.EndContractBlock();
Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultFileStreamBufferSize, FileOptions.SequentialScan, Path.GetFileName(path), false, false, true);
Init(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize, false);
}
Qui abbiamo entrambi i casi in cui un tiro che potrebbe accadere. Per prima cosa abbiamo la validazione contro argomenti fasulli. Dopodiché abbiamo una chiamata al costruttore FileStream
che a sua volta potrebbe generare un'eccezione.
In questo caso, l'eccezione può essere passata.
Ora i casi che dobbiamo considerare sono un po 'più complicati.
Con la maggior parte dei casi di convalida considerati all'inizio di questa risposta, non importava veramente in quale ordine abbiamo fatto le cose. Con un metodo o una proprietà dobbiamo assicurarci di aver cambiato le cose per essere in uno stato valido o di aver generato un'eccezione e lasciato le cose da sole, altrimenti possiamo ancora finire con l'oggetto in uno stato non valido anche se l'eccezione era gettato (per la maggior parte è sufficiente fare tutte le convalide prima di cambiare qualcosa).Con i costruttori non importa molto quale sia l'ordine in cui vengono eseguite le cose, dal momento che non restituiremo un oggetto in quel caso, quindi se buttiamo tutto, non abbiamo inserito alcuna junk nell'applicazione.
Con la chiamata a new FileStream()
tuttavia, potrebbero verificarsi effetti collaterali. È importante che venga tentato solo dopo ogni altro caso in cui venga fatta un'eccezione.
Per la maggior parte di nuovo, questo è facile da fare nella pratica. È naturale mettere prima tutti i tuoi controlli di convalida, e questo è tutto ciò che devi fare il 99% delle volte. Un caso importante è se si sta ottenendo una risorsa non gestita nel corso di un costruttore. Se si lancia un'eccezione in tale costruttore, ciò significa che l'oggetto non è stato costruito. In quanto tale, non sarà finalizzato o eliminato e, in quanto tale, la risorsa non gestita non verrà rilasciata.
Alcune linee guida su come evitare questo:
non utilizzare risorse non gestite direttamente, in primo luogo. Se possibile, lavora attraverso le classi gestite che li avvolgono, quindi è il problema di quell'oggetto.
Se è necessario utilizzare una risorsa non gestita, non fare nient'altro.
Se è necessario disporre di una classe con una risorsa non gestita e un altro stato, combinare le due linee guida sopra riportate; crea una classe wrapper che gestisce solo la risorsa non gestita e la usa all'interno della classe.
- Meglio ancora, utilizzare
SafeHandle
per mantenere il puntatore sulla risorsa non gestita se possibile. Questo riguarda molto bene il lavoro per il punto 2.
Ora. Che dire di prendere un'eccezione?
Possiamo certamente farlo. La domanda è: cosa facciamo quando abbiamo preso qualcosa? Ricorda che dobbiamo creare un oggetto che corrisponda a quello che ci è stato chiesto o generare un'eccezione. La maggior parte delle volte, se una delle cose che abbiamo tentato nel corso di questa operazione fallisce, non c'è nulla che possiamo fare per costruire con successo l'oggetto. Siamo quindi in grado di lasciar passare l'eccezione, o di catturare un'eccezione solo per lanciarne un'altra più adatta dal punto di vista di qualcuno che chiama il costruttore.
Ma certamente se possiamo continuare significativamente dopo un catch
allora è permesso.
Quindi, in tutto, la risposta a "È possibile utilizzare un lancio o provare e catturare all'interno di un costruttore?" è sì".
C'è un unico neo. Come visto sopra, il bello del lancio all'interno di un costruttore è che qualsiasi new
ottiene un oggetto valido oppure viene generata un'eccezione; non c'è niente in mezzo, o hai quell'oggetto o non lo fai.
Un costruttore statico, tuttavia, è un costruttore per la classe nel suo complesso. Se un costruttore di istanze fallisce, non si ottiene un oggetto ma se un costruttore statico fallisce non si ottiene una classe!
Sei praticamente condannato a qualsiasi tentativo futuro di utilizzare quella classe, o qualsiasi derivazione da esso, per il resto della vita dell'applicazione (in senso stretto, per il resto della vita del dominio dell'app).Per la maggior parte questo significa lanciare un'eccezione in una classe statica è una pessima idea. Se è possibile che qualcosa tentato e fallito possa essere tentato e riesca in un altro momento, non dovrebbe essere fatto in un costruttore statico.
Circa l'unica volta in cui si desidera inserire un costruttore statico è quando si desidera che l'applicazione fallisca completamente. Ad esempio, è utile lanciare un'applicazione web priva di un'impostazione di configurazione vitale; certo, è fastidioso che ogni singola richiesta fallisca con lo stesso messaggio di errore, ma ciò significa che si è sicuri di risolvere il problema!
Sì, puoi lanciare e catturare un'eccezione all'interno di un costruttore – Agalo
È generalmente un odore di codice se un costruttore svolge qualsiasi lavoro. Come "unire stanze" ecc. Dovresti spostarlo in una funzione separata –
Vedere, https://stackoverflow.com/questions/77639/when-is-right-for-a-constructor-to-throw-an- eccezione, che è simile, ma non identica a questa domanda, e ha buone risposte. –