Convalida dei parametri del costruttore in C # - Best practice


34

Qual è la migliore pratica per la validazione dei parametri del costruttore?

Supponiamo un po 'di C #:

public class MyClass
{
    public MyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            throw new ArgumentException("Text cannot be empty");

        // continue with normal construction
    }
}

Sarebbe accettabile lanciare un'eccezione?

L'alternativa che ho incontrato è stata la pre-validazione, prima di creare un'istanza:

public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
        {
            MessageBox.Show("Text cannot be empty");
            return null;
        }
        else
        {
            return new MyClass(text);
        }
    }
}

10
"accettabile lanciare un'eccezione?" Qual è l'alternativa?
S. Lott,

10
@ S.Lott: non fare nulla. Imposta un valore predefinito. Stampa un messaggio sulla console / file di registro. Suonare una campana. Accendi una luce.
FrustratedWithFormsDesigner,

3
@FrustratedWithFormsDesigner: "Non fare nulla?" Come sarà soddisfatto il vincolo "testo non vuoto"? "Imposta un valore predefinito"? Come verrà utilizzato il valore dell'argomento di testo? Le altre idee vanno bene, ma quelle due porterebbero a un oggetto in uno stato che viola i vincoli di questa classe.
S. Lott,

1
@ S.Lott Per paura di ripetermi, ero aperto alla possibilità che ci fosse qualche altro approccio, che non conoscevo. Credo di non sapere tutto, questo lo so per certo.
MPelletier,

3
@MPelletier, la convalida anticipata dei parametri finirà per violare DRY. (Non ripetere te stesso) Poiché dovrai copiare quella pre-validazione su qualsiasi codice che tenti di creare un'istanza di questa classe.
CaffGeek,

Risposte:


26

Tendo a eseguire tutte le mie convalide nel costruttore. Questo è un must perché creo quasi sempre oggetti immutabili. Per il tuo caso specifico penso che questo sia accettabile.

if (string.IsNullOrEmpty(text))
    throw new ArgumentException("message", "text");

Se stai usando .NET 4 puoi farlo. Naturalmente questo dipende dal fatto che consideri non valida una stringa che contiene solo spazi bianchi.

if (string.IsNullOrWhiteSpace(text))
    throw new ArgumentException("message", "text");

Non sono sicuro che il suo "caso specifico" sia abbastanza specifico da dare un consiglio così specifico. Non abbiamo idea se abbia senso in questo contesto generare un'eccezione o semplicemente consentire all'oggetto di esistere comunque.
FrustratedWithFormsDesigner,

@FrustratedWithFormsDesigner - Suppongo che tu mi abbia votato per questo? Ovviamente hai ragione sul fatto che sto estrapolando ma chiaramente questo oggetto teorico deve essere non valido se il testo inserito è vuoto. Vorresti davvero chiamare Initper ricevere una sorta di codice di errore quando un'eccezione funzionerà altrettanto bene e forzerà il tuo oggetto a mantenere sempre uno stato valido?
ChaosPandion,

2
Tendo a suddividere il primo caso in due distinte eccezioni: una che verifica la nullità e genera ArgumentNullExceptionun'altra e un'altra che verifica il vuoto e genera una ArgumentException. Consente al chiamante di essere più esigente se lo desidera nella sua gestione delle eccezioni.
Jesse C. Slicer,

1
@Jesse - Ho usato quella tecnica, ma sono ancora sulla recinzione per la sua utilità complessiva.
ChaosPandion,

3
+1 per oggetti immutabili! Li uso anche laddove possibile (trovo personalmente quasi sempre).

22

Molte persone affermano che i costruttori non dovrebbero generare eccezioni. KyleG in questa pagina , per esempio, fa proprio questo. Onestamente, non riesco a pensare a un motivo per cui no.

In C ++, lanciare un'eccezione da un costruttore è una cattiva idea, perché ti lascia con memoria allocata contenente un oggetto non inizializzato a cui non hai riferimenti (cioè è una classica perdita di memoria). Questo è probabilmente il motivo per cui deriva lo stigma: un gruppo di sviluppatori C ++ di vecchia scuola ha messo a metà il loro apprendimento in C # e ha semplicemente applicato ciò che sapevano dal C ++ ad esso. Al contrario, in Objective-C Apple ha separato la fase di allocazione dalla fase di inizializzazione, quindi i costruttori in quella lingua possono generare eccezioni.

C # non può perdere memoria da una chiamata non riuscita a un costruttore. Anche alcune classi nel framework .NET genereranno eccezioni nei loro costruttori.


Stai così arruffando alcune piume con il tuo commento C ++. :)
ChaosPandion,

5
Ehh, C ++ è una grande lingua, ma uno dei miei fastidi ricorrenti è gente che impara bene e poi una lingua scrivono così tutto il tempo . C # non è C ++.
Formica

10
Può ancora essere pericoloso generare eccezioni da un ctor in C # perché il ctor potrebbe aver allocato risorse non gestite che non verranno mai eliminate. E il finalizzatore deve essere scritto per essere difensivo contro lo scenario in cui è finalizzato un oggetto costruito in modo incompleto. Ma questi scenari sono relativamente rari. Un prossimo articolo su C # Dev Center tratterà questi scenari e come codificarli in modo difensivo, quindi guarda quello spazio per i dettagli.
Eric Lippert,

6
eh? lanciare un'eccezione dal ctor è l'unica cosa che puoi fare in c ++ se non puoi costruire l'oggetto, e non c'è motivo che ciò debba causare una perdita di memoria (anche se ovviamente dipende da).
jk.

@jk: Hmm, hai ragione! Anche se sembra un'alternativa è contrassegnare gli oggetti cattivi come "zombi", cosa che apparentemente l'STL fa al posto delle eccezioni: yosefk.com/c++fqa/exceptions.html#fqa-17.2 La soluzione di Objective-C è molto più ordinata.
Formica,

12

Gettare un'eccezione IFF che la classe non può essere messa in uno stato coerente per quanto riguarda il suo uso semantico. Altrimenti no. MAI consentire a un oggetto di esistere in uno stato incoerente. Ciò include non fornire costruttori completi (come avere un costruttore vuoto + inizializza () prima che l'oggetto sia effettivamente completamente costruito) ... SOLO DI NO!

In un pizzico, lo fanno tutti. L'ho fatto l'altro giorno per un oggetto molto usato in un ambito ristretto. Un giorno lungo la strada, io o qualcun altro probabilmente pagherò il prezzo per quella polizza in pratica.

Dovrei notare che per "costruttore" intendo la cosa che il client chiama per costruire l'oggetto. Potrebbe facilmente essere qualcosa di diverso da un vero e proprio costrutto chiamato "Costruttore". Ad esempio, qualcosa del genere in C ++ non violerebbe il principio IMNSHO:

struct funky_object
{
  ...
private:
  funky_object();
  bool initialize(std::string);

  friend boost::optional<funky_object> build_funky(std::string);
};
boost::optional<funky_object> build_funky(std::string str)
{
  funky_object fo;
  if (fo.initialize(str)) return fo;
  return boost::optional<funky_object>();
}

Dal momento che l'unico modo per creare un funky_objectè chiamando build_funkyil principio di non consentire mai l'esistenza di un oggetto non valido rimane intatto anche se il vero "Costruttore" non termina il lavoro.

Questo è un sacco di lavoro extra anche se per guadagno discutibile (forse anche perdita). Preferirei comunque il percorso di eccezione.


9

In questo caso, utilizzerei il metodo factory. Fondamentalmente, impostare la tua classe ha solo costruttori privati ​​e un metodo factory che restituisce un'istanza del tuo oggetto. Se i parametri iniziali non sono validi, è sufficiente restituire null e fare decidere al codice chiamante cosa fare.

public class MyClass
{
    private MyClass(string text)
    {
        //normal construction
    }

    public static MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            return null;
        else
            return new MyClass(text);
    }
}
public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        var cls = MyClass.MakeMyClass(text);
        if(cls == null)
             //show messagebox or throw exception
        return cls;
    }
}

Non gettare mai eccezioni a meno che le condizioni non siano eccezionali. Sto pensando che in questo caso, un valore vuoto può essere passato facilmente. In tal caso, l'utilizzo di questo modello eviterebbe le eccezioni e le penali di prestazione che incorrono mantenendo lo stato MyClass valido.


1
Non vedo il vantaggio. Perché testare il risultato di una factory per null è preferibile avere un try-catch da qualche parte in grado di catturare l'eccezione dal costruttore? Il tuo approccio sembra essere "ignora le eccezioni, torna a controllare esplicitamente il valore restituito dai metodi per gli errori". Abbiamo accettato da tempo che le eccezioni sono generalmente superiori alla necessità di testare esplicitamente ogni valore di ritorno del metodo per individuare eventuali errori, quindi il tuo consiglio sembra sospetto. Vorrei vedere una giustificazione del perché pensi che la regola generale non si applichi qui.
DW,

non abbiamo mai accettato che le eccezioni fossero superiori ai codici di ritorno, possono essere un enorme successo se lanciate troppo spesso. Tuttavia, il lancio di un'eccezione da un costruttore perderà la memoria, anche in .NET, se hai allocato qualcosa che non è gestito. Devi fare molto lavoro nel tuo finalizzatore per compensare questo caso. Comunque, la fabbrica è una buona idea qui, e TBH dovrebbe lanciare un'eccezione invece di restituire un valore nullo. Supponendo di creare solo poche classi "cattive", è preferibile effettuare il lancio di fabbrica.
gbjbaanb,

2
  • Un costruttore non dovrebbe avere effetti collaterali.
    • Qualsiasi cosa oltre all'inizializzazione del campo privato dovrebbe essere vista come un effetto collaterale.
    • Un costruttore con effetti collaterali infrange il principio di responsabilità singola (SRP) e va contro lo spirito della programmazione orientata agli oggetti (OOP).
  • Un costruttore dovrebbe essere leggero e non dovrebbe mai fallire.
    • Ad esempio, rabbrividisco sempre quando vedo un blocco try-catch all'interno di un costruttore. Un costruttore non dovrebbe generare eccezioni o errori di registro.

Si potrebbe ragionevolmente mettere in discussione queste linee guida e dire: "Ma io non seguo queste regole e il mio codice funziona bene!" A quello, risponderei, "Beh, potrebbe essere vero, fino a quando non lo è."

  • Eccezioni ed errori all'interno di un costruttore sono molto inaspettati. A meno che non gli venga detto di farlo, i futuri programmatori non saranno inclini a circondare queste chiamate del costruttore con un codice difensivo.
  • Se qualcosa non riesce nella produzione, la traccia dello stack generata potrebbe essere difficile da analizzare. La parte superiore della traccia dello stack può puntare alla chiamata del costruttore, ma molte cose accadono nel costruttore e potrebbe non puntare al LOC effettivo non riuscito.
    • Ho analizzato molte tracce dello stack .NET in cui questo era il caso.

0

Dipende da cosa sta facendo MyClass. Se MyClass fosse in realtà una classe di repository di dati e il testo dei parametri fosse una stringa di connessione, la migliore pratica sarebbe quella di lanciare ArgumentException. Tuttavia, se MyClass è la classe StringBuilder (ad esempio), è possibile lasciarlo vuoto.

Quindi dipende da quanto sia essenziale il testo del parametro per il metodo: l'oggetto ha senso con un valore nullo o vuoto?


2
Penso che la domanda implichi che la classe in realtà richiede che la stringa non sia vuota. Questo non è il caso nell'esempio StringBuilder.
Sergio Acosta,

Quindi la risposta deve essere sì: è accettabile generare un'eccezione. Per evitare di dover provare / rilevare questo errore, è possibile utilizzare un modello di fabbrica per gestire la creazione dell'oggetto per te, che includerebbe un caso stringa vuoto.
Steven Striga,

0

La mia preferenza è quella di impostare un valore predefinito, ma so che Java ha la libreria "Apache Commons" che fa qualcosa del genere, e penso che sia anche una buona idea. Non vedo alcun problema con il lancio di un'eccezione se il valore non valido metterebbe l'oggetto in uno stato inutilizzabile; una stringa non è un buon esempio, ma se fosse per la DI del povero? Non potresti operare se un valore di nullfosse passato al posto di, diciamo, ICustomerRepositoryun'interfaccia. In una situazione come questa, dire un'eccezione è il modo corretto di gestire le cose.


-1

Quando lavoravo in c ++, non usavo generare eccezioni nel costruttore a causa del problema di perdita di memoria che può causare, l'avevo imparato nel modo più duro. Chiunque lavori con c ++ sa quanto possa essere difficile e problematica la perdita di memoria.

Ma se sei in c # / Java, allora non hai questo problema, perché Garbage Collector raccoglierà la memoria. Se stai usando C #, penso che sia preferibile usare la proprietà per un modo piacevole e coerente per assicurarsi che i vincoli dei dati siano garantiti.

public class MyClass
{
    private string _text;
    public string Text 
    {
        get
        {
            return _text;
        } 
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Text cannot be empty");
            _text = value;
        } 
    }

    public MyClass(string text)
    {
        Text = text;
        // continue with normal construction
    }
}

-3

Lanciare un'eccezione sarà un vero dolore per gli utenti della tua classe. Se la convalida è un problema, generalmente faccio in modo che la creazione dell'oggetto sia un processo in 2 fasi.

1 - Crea l'istanza

2 - Chiama un metodo Initialize con un risultato di ritorno.

O

Chiama un metodo / funzione che crea la classe per te che può eseguire la convalida.

O

Avere una bandiera isValid, che non mi piace particolarmente, ma alcune persone lo fanno.


3
@Dunk, è solo sbagliato.
CaffGeek,

4
@Chad, questa è la tua opinione, ma la mia opinione non è sbagliata. È solo diverso dal tuo. Trovo il codice try-catch molto più difficile da leggere e ho visto molti più errori creati quando le persone usano try-catch per la gestione degli errori banale rispetto ai metodi più tradizionali. Questa è stata la mia esperienza e non è sbagliata. È quello che è.
Dunk

5
Il PUNTO è che dopo che chiamo un costruttore, se non viene creato perché un input non è valido, questa è un'ECCEZIONE. I dati dell'app ora sono in uno stato incoerente / non valido se creo semplicemente l'oggetto e imposto un flag che dice isValid = false. Dover testare dopo la creazione di una classe se è valido è un progetto orribile, molto soggetto a errori. E, hai detto, avere un costruttore, quindi chiamare l'inizializzazione ... E se non chiamassi l'inizializzazione? Cosa poi? E se Initialize ottiene dati errati, posso generare un'eccezione ora? La tua lezione non dovrebbe essere difficile da usare.
CaffGeek,

5
@Dunk, se trovi delle eccezioni difficili da usare, le stai usando male. In poche parole, un cattivo input è stato fornito a un costruttore. Questa è un'eccezione. L'app non dovrebbe continuare a funzionare a meno che non sia corretta. Periodo. In tal caso, si ottiene un problema peggiore, dati errati. Se non sai come gestire l'eccezione, non lo fai, lasci che si riempia lo stack di chiamate fino a quando non può essere gestito, anche se ciò significa semplicemente registrare e interrompere l'esecuzione. In poche parole, non è necessario gestire le eccezioni o testarle. Funzionano semplicemente.
CaffGeek,

3
Ci dispiace, questo è troppo anti-pattern da considerare.
MPelletier,
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.