Inizializzare i campi di classe nel costruttore o alla dichiarazione?


413

Di recente ho programmato in C # e Java e sono curioso di sapere dove sia il posto migliore per inizializzare i miei campi di classe.

Devo farlo alla dichiarazione ?:

public class Dice
{
    private int topFace = 1;
    private Random myRand = new Random();

    public void Roll()
    {
       // ......
    }
}

o in un costruttore ?:

public class Dice
{
    private int topFace;
    private Random myRand;

    public Dice()
    {
        topFace = 1;
        myRand = new Random();
    }

    public void Roll()
    {
        // .....
    }
}

Sono davvero curioso di ciò che alcuni di voi veterani pensano sia la migliore pratica. Voglio essere coerente e attenermi a un approccio.


3
Nota che per le strutture non puoi avere inizializzatori di campi di istanza, quindi non hai altra scelta che usare il costruttore.
yo

Risposte:


310

Le mie regole:

  1. Non inizializzare con i valori di default in dichiarazione ( null, false, 0, 0.0...).
  2. Preferisci l'inizializzazione nella dichiarazione se non hai un parametro costruttore che modifica il valore del campo.
  3. Se il valore del campo cambia a causa di un parametro del costruttore, inserire l'inizializzazione nei costruttori.
  4. Sii coerente nella tua pratica (la regola più importante).

4
Mi aspetto che kokos significhi che non dovresti inizializzare i membri ai loro valori predefiniti (0, false, null ecc.), Poiché il compilatore lo farà per te (1.). Ma se vuoi inizializzare un campo a qualcosa di diverso dal suo valore predefinito, dovresti farlo nella dichiarazione (2.). Penso che potrebbe essere l'uso della parola "predefinito" che ti confonde.
Ricky Helgesson,

95
Non sono d'accordo con la regola 1 - non specificando un valore predefinito (indipendentemente dal fatto che sia stato inizializzato dal compilatore o meno) stai lasciando allo sviluppatore indovinare o andare alla ricerca della documentazione su quale sarebbe il valore predefinito per quella particolare lingua. Ai fini della leggibilità, vorrei sempre specificare il valore predefinito.
James

32
Il valore predefinito di un tipo default(T)è sempre il valore che ha una rappresentazione binaria interna di 0.
Olivier Jacot-Descombes,

15
Indipendentemente dal fatto che ti piaccia o meno la regola 1, non può essere utilizzata con campi di sola lettura, che devono essere esplicitamente inizializzati al termine del costruttore.
yoyo

36
Non sono d'accordo con coloro che non sono d'accordo con la regola 1. Va bene aspettarsi che gli altri imparino il linguaggio C #. Proprio come non commentiamo ogni foreachciclo con "Questo ripete quanto segue per tutti gli elementi nella lista", non abbiamo bisogno di riaffermare costantemente i valori predefiniti di C #. Inoltre, non è necessario fingere che C # abbia una semantica non inizializzata. Poiché l'assenza di un valore ha un significato chiaro, va bene lasciarlo fuori. Se è l'ideale per essere espliciti, è necessario utilizzare sempre newquando si creano nuovi delegati (come richiesto in C # 1). Ma chi lo fa? La lingua è progettata per programmatori coscienziosi.
Edward Brey,

149

In C # non importa. I due esempi di codice forniti sono assolutamente equivalenti. Nel primo esempio il compilatore C # (o è CLR?) Costruirà un costruttore vuoto e inizializzerà le variabili come se fossero nel costruttore (c'è una leggera sfumatura a ciò che Jon Skeet spiega nei commenti qui sotto). Se esiste già un costruttore, qualsiasi inizializzazione "sopra" verrà spostata nella parte superiore di esso.

In termini di buone pratiche, il primo è meno soggetto a errori rispetto al secondo, poiché qualcuno potrebbe facilmente aggiungere un altro costruttore e dimenticare di incatenarlo.


4
Ciò non è corretto se si sceglie di inizializzare la classe con GetUninitializedObject. Qualunque cosa ci sia nel ctor non verrà toccata ma verranno eseguite le dichiarazioni sul campo.
Wolf5,

28
In realtà importa. Se il costruttore della classe base chiama un metodo virtuale (che è generalmente una cattiva idea, ma può succedere) che viene sovrascritto nella classe derivata, quindi usando gli inizializzatori di variabili di istanza la variabile verrà inizializzata quando viene chiamato il metodo - mentre usando l'inizializzazione nel costruttore, non lo saranno. (Gli inizializzatori di variabili di istanza vengono eseguiti prima che venga chiamato il costruttore della classe base.)
Jon Skeet,

2
Veramente? Giuro di aver preso queste informazioni dal CLR di Richter tramite C # (2a edizione credo) e il fatto era che si trattava di zucchero sintattico (potrei averlo letto male?) E il CLR ha semplicemente bloccato le variabili nel costruttore. Ma stai affermando che non è così, cioè l'inizializzazione del membro può sparare prima dell'inizializzazione del ctor nello scenario folle di chiamare un ctor virtuale in base e avere un override nella classe in questione. Ho capito bene? L'hai appena scoperto? Perplesso su questo commento aggiornato su un post di 5 anni (OMG sono passati 5 anni?).
Quibblesome

@Quibblesome: un costruttore di classe figlio conterrà una chiamata concatenata al costruttore principale. Un compilatore di linguaggi è libero di includere tanto o meno codice prima di quello desiderato, a condizione che il costruttore principale venga chiamato esattamente una volta su tutti i percorsi di codice e che venga fatto un uso limitato dell'oggetto in costruzione prima di quella chiamata. Uno dei miei fastidi con C # è che mentre può generare codice che inizializza i campi e codice che utilizza parametri del costruttore, non offre alcun meccanismo per inizializzare i campi in base ai parametri del costruttore.
supercat,

1
@Quibblesome, il primo sarà un problema, se il valore viene assegnato e passato a un metodo WCF. Il che ripristinerà i dati al tipo di dati predefinito del modello. La migliore pratica è fare l'inizializzazione nel costruttore (in seguito).
Pranesh Janarthanan,

16

La semantica di C # differisce leggermente da Java qui. In C # l'assegnazione nella dichiarazione viene eseguita prima di chiamare il costruttore della superclasse. In Java viene eseguito immediatamente dopo il quale consente di utilizzare 'this' (particolarmente utile per le classi interne anonime), e significa che la semantica delle due forme corrisponde davvero.

Se puoi, rendi i campi definitivi.


15

Penso che ci sia un avvertimento. Una volta ho commesso un errore del genere: all'interno di una classe derivata, ho cercato di "inizializzare alla dichiarazione" i campi ereditati da una classe base astratta. Il risultato fu che esistevano due serie di campi, uno è "base" e un altro quelli appena dichiarati, e mi è costato un po 'di tempo per eseguire il debug.

La lezione: per inizializzare i campi ereditati , lo faresti all'interno del costruttore.


Quindi, se fai riferimento al campo per derivedObject.InheritedField, si riferisce al tuo di base o a quello derivato?
RayLuo,

6

Supponendo il tipo nel tuo esempio, preferisci decisamente inizializzare i campi nel costruttore. I casi eccezionali sono:

  • Campi in classi / metodi statici
  • Campi digitati come statici / finali / et al

Penso sempre all'elenco dei campi nella parte superiore di una classe come al sommario (ciò che è contenuto nel presente documento, non al modo in cui viene utilizzato) e al costruttore come introduzione. I metodi ovviamente sono capitoli.


Perché è "sicuramente" così? Hai fornito solo una preferenza di stile, senza spiegarne il ragionamento. Oh aspetta, non importa, secondo la risposta di @quibblesome , sono "assolutamente equivalenti", quindi è davvero solo una preferenza di stile personale.
RayLuo,

4

E se te lo dicessi, dipende?

In generale, inizializzo tutto e lo faccio in modo coerente. Sì, è eccessivamente esplicito ma è anche un po 'più facile da mantenere.

Se siamo preoccupati per le prestazioni, allora inizializzo solo ciò che deve essere fatto e lo posiziono nelle aree che danno il massimo.

In un sistema in tempo reale, mi chiedo se ho persino bisogno della variabile o della costante.

E in C ++ spesso faccio quasi nessuna inizializzazione in nessuno dei due posti e lo sposto in una funzione Init (). Perché? Bene, in C ++ se stai inizializzando qualcosa che può generare un'eccezione durante la costruzione di oggetti, ti apri a perdite di memoria.


4

Ci sono molte e varie situazioni.

Ho solo bisogno di un elenco vuoto

La situazione è chiara Devo solo preparare il mio elenco e impedire che venga generata un'eccezione quando qualcuno aggiunge un elemento all'elenco.

public class CsvFile
{
    private List<CsvRow> lines = new List<CsvRow>();

    public CsvFile()
    {
    }
}

Conosco i valori

So esattamente quali valori voglio avere di default o devo usare qualche altra logica.

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = new List<string>() {"usernameA", "usernameB"};
    }
}

o

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = GetDefaultUsers(2);
    }
}

Elenco vuoto con possibili valori

A volte mi aspetto un elenco vuoto per impostazione predefinita con la possibilità di aggiungere valori tramite un altro costruttore.

public class AdminTeam
{
    private List<string> usernames = new List<string>();

    public AdminTeam()
    {
    }

    public AdminTeam(List<string> admins)
    {
         admins.ForEach(x => usernames.Add(x));
    }
}

3

In Java, un inizializzatore con la dichiarazione indica che il campo viene sempre inizializzato allo stesso modo, indipendentemente dal costruttore utilizzato (se ne hai più di uno) o dai parametri dei costruttori (se hanno argomenti), sebbene un costruttore possa successivamente cambia il valore (se non è definitivo). Pertanto, l'utilizzo di un inizializzatore con una dichiarazione suggerisce a un lettore che il valore inizializzato è il valore che il campo ha in tutti i casi , indipendentemente dal costruttore utilizzato e indipendentemente dai parametri passati a qualsiasi costruttore. Pertanto, utilizzare un inizializzatore con la dichiarazione solo se, e sempre se, il valore per tutti gli oggetti costruiti è lo stesso.


3

Il design di C # suggerisce che l'inizializzazione in linea è preferita, o non sarebbe nella lingua. Ogni volta che puoi evitare un riferimento incrociato tra diversi punti del codice, in genere stai meglio.

C'è anche la questione della coerenza con l'inizializzazione del campo statico, che deve essere in linea per le migliori prestazioni. Le Linee guida per la progettazione del framework per la progettazione di costruttori affermano quanto segue:

✓ CONSIDERA l'inizializzazione dei campi statici in linea piuttosto che l'utilizzo esplicito di costruttori statici, poiché il runtime è in grado di ottimizzare le prestazioni di tipi che non hanno un costruttore statico esplicitamente definito.

"Considerare" in questo contesto significa farlo a meno che non ci sia una buona ragione per non farlo. Nel caso dei campi di inizializzazione statica, un buon motivo potrebbe essere se l'inizializzazione è troppo complessa per essere codificata in linea.


2

Essere coerenti è importante, ma questa è la domanda da porsi: "Ho un costruttore per qualcos'altro?"

In genere, sto creando modelli per i trasferimenti di dati che la classe stessa non fa altro che lavorare come alloggiamento per variabili.

In questi scenari, di solito non ho metodi o costruttori. Mi sembrerebbe sciocco creare un costruttore con lo scopo esclusivo di inizializzare le mie liste, soprattutto perché posso inizializzarle in linea con la dichiarazione.

Quindi, come molti altri hanno già detto, dipende dal tuo utilizzo. Mantienilo semplice e non fare nulla in più che non devi.


2

Considera la situazione in cui hai più di un costruttore. L'inizializzazione sarà diversa per i diversi costruttori? Se saranno uguali, allora perché ripetere per ciascun costruttore? Questo è in linea con l'istruzione kokos, ma potrebbe non essere correlato ai parametri. Diciamo, ad esempio, che vuoi mantenere un flag che mostra come è stato creato l'oggetto. Quindi quel flag verrebbe inizializzato in modo diverso per costruttori diversi indipendentemente dai parametri del costruttore. D'altra parte, se si ripete la stessa inizializzazione per ciascun costruttore, si lascia la possibilità di modificare (involontariamente) il parametro di inizializzazione in alcuni costruttori, ma non in altri. Quindi, il concetto di base qui è che il codice comune dovrebbe avere una posizione comune e non essere potenzialmente ripetuto in posizioni diverse.


1

Vi è un leggero vantaggio in termini di prestazioni nell'impostazione del valore nella dichiarazione. Se lo si imposta nel costruttore, in realtà viene impostato due volte (prima sul valore predefinito, quindi reimpostato nel ctor).


2
In C #, i campi vengono sempre impostati prima il valore predefinito. La presenza di un inizializzatore non fa differenza.
Jeffrey L Whitledge,

0

Di solito provo il costruttore a non fare altro che ottenere le dipendenze e inizializzare i membri dell'istanza correlati con loro. Questo ti renderà la vita più facile se vuoi mettere alla prova le tue lezioni.

Se il valore che si intende assegnare a una variabile di istanza non viene influenzato da nessuno dei parametri che si intende passare al costruttore, assegnarlo al momento della dichiarazione.


0

Non una risposta diretta alla tua domanda sulle migliori pratiche, ma un importante e relativo punto di aggiornamento è che nel caso di una definizione di classe generica, o lasciarla sul compilatore per inizializzare con i valori predefiniti o dobbiamo usare un metodo speciale per inizializzare i campi ai loro valori predefiniti (se ciò è assolutamente necessario per la leggibilità del codice).

class MyGeneric<T>
{
    T data;
    //T data = ""; // <-- ERROR
    //T data = 0; // <-- ERROR
    //T data = null; // <-- ERROR        

    public MyGeneric()
    {
        // All of the above errors would be errors here in constructor as well
    }
}

E il metodo speciale per inizializzare un campo generico al suo valore predefinito è il seguente:

class MyGeneric<T>
{
    T data = default(T);

    public MyGeneric()
    {           
        // The same method can be used here in constructor
    }
}

0

Quando non hai bisogno di logica o gestione degli errori:

  • Inizializza i campi di classe alla dichiarazione

Quando è necessaria una gestione logica o degli errori:

  • Inizializza i campi di classe nel costruttore

Funziona bene quando il valore di inizializzazione è disponibile e l'inizializzazione può essere inserita su una riga. Tuttavia, questa forma di inizializzazione presenta delle limitazioni a causa della sua semplicità. Se l'inizializzazione richiede un po 'di logica (ad esempio, la gestione degli errori o un ciclo for per riempire un array complesso), l'assegnazione semplice è inadeguata. Le variabili di istanza possono essere inizializzate nei costruttori, dove è possibile utilizzare la gestione degli errori o altra logica.

Da https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html .

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.