2016-02-12 16 views
5

È possibile utilizzare un throw o try-catch all'interno di un costruttore?
In tal caso, qual è lo scopo di avere un costruttore che ha un argomento che può generare un'eccezione?Gestione delle eccezioni all'interno di un costruttore

Questo costruttore è un esempio:

public Chat() 
{ 
    chatClient = new Client(Configuration.LoginEmail, Configuration.LoginPassword); 
    chatRoom = chatClient.JoinRoom(Configuration.RoomUrl); 
} 

La linea chatRoom = chatClient.JoinRoom(Configuration.RoomUrl); possono gettare eccezioni.

+0

Sì, puoi lanciare e catturare un'eccezione all'interno di un costruttore – Agalo

+0

È generalmente un odore di codice se un costruttore svolge qualsiasi lavoro. Come "unire stanze" ecc. Dovresti spostarlo in una funzione separata –

+1

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. –

risposta

11

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).

  1. Il chiamante fornisce valori che è possibile utilizzare direttamente.

Eh, usali.

  1. 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.

  1. 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:

  1. Il significato è univoco? Se c'è più di un modo per considerare cosa significa "veramente", allora lancia un'eccezione.

  2. È 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:

  1. 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.

  2. 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.

  1. 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!

+1

Questo è il commento più sorprendente in decompressione che abbia mai visto [@JonHanna Mi piacerebbe che tu scrivessi una risposta con qualche dettaglio del tuo commento. Queste risposte erano esattamente ciò che stavo cercando:)] (http://stackoverflow.com/questions/35373776/managing-exception-handling-with-a-constructor#comment58455586_35373776) grazie per esserti sforzato così tanto: D –

+2

Probabilmente avrei potuto coprire lo stesso contenuto in modo più conciso, ma è stato scritto in modo frammentario tra le faccende e le discussioni tra bambini, quindi c'è un elemento delle scuse di Pascal per scrivere una lunga lettera perché non ha avuto il tempo di scriverne una breve. –

+0

Ho intenzione di mettere una taglia su di esso per premiarti .. Sarà solo un token rispetto al tuo rappresentante, ma è apprezzato. e hai un grande senso dell'umorismo: D –

2

È possibile utilizzare un lancio o provare a catturare all'interno di un costruttore?

Entrambi sono possibili.

Se si verifica un'eccezione durante la creazione di un'istanza di oggetto, e c'è qualcosa che puoi fare su di esso, prenderlo e provare a risolverlo.

Se non vi è nulla da fare sull'eccezione, generalmente è meglio consentirne la propagazione (anziché lasciare l'istanza dell'oggetto in uno stato inizializzato in modo errato).

1

Il normale ciclo di vita di un oggetto viene interrotto se viene generata un'eccezione in un costruttore di istanze. Quindi se l'oggetto ha un distruttore per ripulire qualsiasi risorsa, allora un'eccezione generata nel costruttore impedirà l'esecuzione del distruttore e quindi si verificherà una perdita di memoria. Prendi sempre, ripulisci e rilancia se disponi di risorse usa e getta allocate nei costruttori o per qualsiasi campo.

Se un'eccezione viene generata in un costruttore statico, la classe non è più in uno stato che può essere utilizzato dallo AppDomain. Quindi cerca sempre le eccezioni in costruttori statici e gestiscili. Do non ri-gettare e fare non lasciarli non catturati.

Le eccezioni di cattura e gestione in qualsiasi costruttore vanno bene.

Ma come per normale, è 1000 volte meglio codificare per evitare le eccezioni piuttosto che farle accadere.

Vedere MSDN Constructor Design & Eric Lippert's Vexing Exceptions.

+1

Potrebbe essere meglio evitare le eccezioni piuttosto che lasciarle accadere, ma quando sei sul punto di scrivere un costruttore e pensare a cosa fare quando ti vengono dati argomenti sbagliati, è 1000 volte meglio lanciare un'eccezione prova a donnela attorno ad esso. Se il risultato è negativo, allora lascia che il chiamante aggiusti il ​​disordine piuttosto che spazzarlo sotto il tappeto. –

+0

Hey, grazie per questo e i link sono fantastici. Volevo sapere cosa avevano da dire gli esperti ... e la discussione nei commenti è interessante. –