Initialiser les champs de classe dans le constructeur ou lors de la déclaration?

413

J'ai récemment programmé en C # et Java et je suis curieux de savoir où le meilleur endroit est d'initialiser mes champs de classe.

Dois-je le faire lors de la déclaration?:

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

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

ou chez un constructeur?:

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

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

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

Je suis vraiment curieux de savoir ce que certains d'entre vous, anciens combattants, pensez être la meilleure pratique. Je veux être cohérent et m'en tenir à une seule approche.

mmcdole
la source
3
Notez que pour les structures, vous ne pouvez pas avoir d'initialiseurs de champ d'instance, vous n'avez donc pas d'autre choix que d'utiliser le constructeur.
yoyo
Depuis que vous avez évoqué les "meilleures pratiques": satisfice.com/blog/archives/5164 , forbes.com/sites/mikemyatt/2012/08/15/best-practices-arent/…
Stephen C

Réponses:

310

Mes règles:

  1. Ne pas initialiser avec les valeurs par défaut dans la déclaration ( null, false, 0, 0.0...).
  2. Préférez l'initialisation dans la déclaration si vous n'avez pas de paramètre constructeur qui modifie la valeur du champ.
  3. Si la valeur du champ change en raison d'un paramètre constructeur, placez l'initialisation dans les constructeurs.
  4. Soyez cohérent dans votre pratique (la règle la plus importante).
kokos
la source
4
Je m'attends à ce que kokos signifie que vous ne devez pas initialiser les membres à leurs valeurs par défaut (0, faux, null, etc.), car le compilateur le fera pour vous (1.). Mais si vous souhaitez initialiser un champ à autre chose que sa valeur par défaut, vous devez le faire dans la déclaration (2.). Je pense que c'est peut-être l'utilisation du mot «par défaut» qui vous déroute.
Ricky Helgesson
95
Je ne suis pas d'accord avec la règle 1 - en ne spécifiant pas de valeur par défaut (qu'elle soit initialisée par le compilateur ou non), vous laissez le développeur deviner ou aller chercher la documentation sur la valeur par défaut de cette langue particulière. Pour des raisons de lisibilité, je spécifierais toujours la valeur par défaut.
James
32
La valeur par défaut d'un type default(T)est toujours la valeur qui a une représentation binaire interne de 0.
Olivier Jacot-Descombes
15
Que vous aimiez ou non la règle 1, elle ne peut pas être utilisée avec des champs en lecture seule, qui doivent être explicitement initialisés au moment où le constructeur est terminé.
yoyo
36
Je suis en désaccord avec ceux qui ne sont pas d'accord avec la règle 1. Il est normal que les autres apprennent le langage C #. Tout comme nous ne commentons pas chaque foreachboucle avec "cela répète ce qui suit pour tous les éléments de la liste", nous n'avons pas besoin de reformuler constamment les valeurs par défaut de C #. Nous n'avons pas non plus besoin de prétendre que C # a une sémantique non initialisée. Étant donné que l'absence d'une valeur a une signification claire, il est bon de la laisser de côté. Si l'idéal est d'être explicite, vous devez toujours l'utiliser newlors de la création de nouveaux délégués (comme cela était requis en C # 1). Mais qui fait ça? Le langage est conçu pour les codeurs consciencieux.
Edward Brey
149

En C #, cela n'a pas d'importance. Les deux exemples de code que vous donnez sont totalement équivalents. Dans le premier exemple, le compilateur C # (ou est-ce le CLR?) Va construire un constructeur vide et initialiser les variables comme si elles étaient dans le constructeur (il y a une légère nuance à cela que Jon Skeet explique dans les commentaires ci-dessous). S'il existe déjà un constructeur, toute initialisation "ci-dessus" sera déplacée en haut de celui-ci.

En termes de meilleures pratiques, le premier est moins sujet aux erreurs que le second, car quelqu'un pourrait facilement ajouter un autre constructeur et oublier de l'enchaîner.

Quibblesome
la source
4
Ce n'est pas correct si vous choisissez d'initialiser la classe avec GetUninitializedObject. Tout ce qui se trouve dans le ctor ne sera pas touché mais les déclarations de champ seront exécutées.
Wolf5
28
En fait, cela compte. Si le constructeur de la classe de base appelle une méthode virtuelle (ce qui est généralement une mauvaise idée, mais peut se produire) qui est remplacée dans la classe dérivée, alors en utilisant les initialiseurs de variable d'instance, la variable sera initialisée lorsque la méthode est appelée - alors que l'utilisation de l'initialisation dans le constructeur, ils ne le seront pas. (Les initialiseurs de variable d'instance sont exécutés avant l'appel du constructeur de la classe de base.)
Jon Skeet
2
Vraiment? Je jure que j'ai saisi cette information du CLR de Richter via C # (2e édition, je pense) et l'essentiel était que c'était du sucre syntaxique (je l'ai peut-être mal lu?) Et le CLR a juste brouillé les variables dans le constructeur. Mais vous dites que ce n'est pas le cas, c'est-à-dire que l'initialisation du membre peut se déclencher avant l'initialisation de ctor dans le scénario fou d'appeler un ctor virtuel dans la base et d'avoir un remplacement dans la classe en question. Ai-je bien compris? Vous venez de découvrir cela? Intrigué par ce commentaire à jour sur un post de 5 ans (OMG, ça fait 5 ans?).
Quibblesome
@Quibblesome: un constructeur de classe enfant contiendra un appel chaîné au constructeur parent. Un compilateur de langage est libre d'inclure autant ou aussi peu de code avant qu'il le souhaite, à condition que le constructeur parent soit appelé exactement une fois sur tous les chemins de code, et que l'utilisation en cours soit limitée à l'objet en construction avant cet appel. Un de mes ennuis avec C # est que même s'il peut générer du code qui initialise des champs et du code qui utilise des paramètres de constructeur, il n'offre aucun mécanisme pour initialiser des champs basés sur des paramètres de constructeur.
supercat
1
@Quibblesome, l'ancien sera un problème si la valeur est affectée et passée à une méthode WCF. Ce qui réinitialisera les données au type de données par défaut du modèle. La meilleure pratique est de faire l'initialisation dans constructeur (plus tard).
Pranesh Janarthanan
16

La sémantique de C # diffère légèrement de Java ici. En C #, l'affectation dans la déclaration est effectuée avant d'appeler le constructeur de la superclasse. En Java, cela se fait immédiatement après ce qui permet d'utiliser 'this' (particulièrement utile pour les classes internes anonymes), et signifie que la sémantique des deux formes correspond vraiment.

Si vous le pouvez, définissez les champs de manière définitive.

Tom Hawtin - sellerie
la source
15

Je pense qu'il y a une mise en garde. J'ai commis une fois une telle erreur: à l'intérieur d'une classe dérivée, j'ai essayé "d'initialiser à la déclaration" les champs hérités d'une classe de base abstraite. Le résultat a été qu'il existait deux ensembles de champs, l'un est "base" et l'autre est celui nouvellement déclaré, et il m'a fallu un certain temps pour déboguer.

La leçon: pour initialiser des champs hérités , vous le feriez à l'intérieur du constructeur.

xji
la source
Donc, si vous référez le champ par derivedObject.InheritedField, se réfère-t-il à votre base ou dérivée?
RayLuo
6

En supposant le type dans votre exemple, préférez certainement initialiser les champs dans le constructeur. Les cas exceptionnels sont:

  • Champs dans les classes / méthodes statiques
  • Champs typés comme statique / final / et al

Je pense toujours à la liste des champs en haut d'une classe comme la table des matières (ce qui est contenu ici, pas comment il est utilisé), et le constructeur comme l'introduction. Les méthodes sont bien sûr des chapitres.

Noel
la source
Pourquoi en est-il "définitivement" ainsi? Vous n'avez fourni qu'une préférence de style, sans expliquer son raisonnement. Oh , attends, jamais l' esprit, selon @quibblesome de » la réponse , ils sont « tout à fait équivalent », donc il est vraiment juste une préférence de style personnel.
RayLuo
4

Et si je te le disais, ça dépend?

En général, j'initialise tout et je le fais de manière cohérente. Oui, c'est trop explicite, mais c'est aussi un peu plus facile à entretenir.

Si nous nous inquiétons des performances, eh bien, je n'initialise que ce qui doit être fait et je le place dans les zones où il en a le plus pour son argent.

Dans un système en temps réel, je me demande si j'ai même besoin de la variable ou de la constante.

Et en C ++, je ne fais souvent presque aucune initialisation à aucun endroit et je le déplace dans une fonction Init (). Pourquoi? Eh bien, en C ++ si vous initialisez quelque chose qui peut lever une exception lors de la construction d'un objet, vous vous ouvrez aux fuites de mémoire.

Dan Blair
la source
4

Les situations sont nombreuses et diverses.

J'ai juste besoin d'une liste vide

La situation est claire. J'ai juste besoin de préparer ma liste et d'empêcher qu'une exception ne soit levée lorsque quelqu'un ajoute un élément à la liste.

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

    public CsvFile()
    {
    }
}

Je connais les valeurs

Je sais exactement quelles valeurs je veux avoir par défaut ou je dois utiliser une autre logique.

public class AdminTeam
{
    private List<string> usernames;

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

ou

public class AdminTeam
{
    private List<string> usernames;

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

Liste vide avec valeurs possibles

Parfois, j'attends une liste vide par défaut avec la possibilité d'ajouter des valeurs via un autre constructeur.

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

    public AdminTeam()
    {
    }

    public AdminTeam(List<string> admins)
    {
         admins.ForEach(x => usernames.Add(x));
    }
}
Miroslav Holec
la source
3

En Java, un initialiseur avec la déclaration signifie que le champ est toujours initialisé de la même manière, quel que soit le constructeur utilisé (si vous en avez plusieurs) ou les paramètres de vos constructeurs (s'ils ont des arguments), bien qu'un constructeur puisse par la suite changer la valeur (si elle n'est pas définitive). Ainsi, l'utilisation d'un initialiseur avec une déclaration suggère au lecteur que la valeur initialisée est la valeur que le champ a dans tous les cas , quel que soit le constructeur utilisé et quels que soient les paramètres transmis à n'importe quel constructeur. Par conséquent, utilisez un initialiseur avec la déclaration uniquement si et toujours si la valeur de tous les objets construits est la même.

Raedwald
la source
3

La conception de C # suggère que l'initialisation en ligne est préférée, ou ce ne serait pas dans le langage. Chaque fois que vous pouvez éviter une référence croisée entre différents endroits du code, vous êtes généralement mieux loti.

Il y a aussi la question de la cohérence avec l'initialisation de champ statique, qui doit être en ligne pour de meilleures performances. Les directives de conception de cadre pour la conception de constructeur disent ceci:

✓ CONSIDÉRER l'initialisation des champs statiques en ligne plutôt que d'utiliser explicitement des constructeurs statiques, car le runtime est en mesure d'optimiser les performances des types qui n'ont pas de constructeur statique explicitement défini.

«Considérer» dans ce contexte signifie le faire à moins qu'il y ait une bonne raison de ne pas le faire. Dans le cas des champs d'initialisation statiques, une bonne raison serait que l'initialisation est trop complexe pour être codée en ligne.

Edward Brey
la source
2

Être cohérent est important, mais c'est la question à se poser: "Ai-je un constructeur pour autre chose?"

En règle générale, je crée des modèles de transfert de données que la classe elle-même ne fait que travailler comme logement pour les variables.

Dans ces scénarios, je n'ai généralement pas de méthodes ni de constructeurs. Il serait ridicule pour moi de créer un constructeur dans le seul but d'initialiser mes listes, d'autant plus que je peux les initialiser en ligne avec la déclaration.

Donc, comme beaucoup d'autres l'ont dit, cela dépend de votre utilisation. Restez simple et ne faites rien de plus que vous n'avez pas à le faire.

MeanJerry
la source
2

Considérez la situation où vous avez plus d'un constructeur. L'initialisation sera-t-elle différente pour les différents constructeurs? S'ils sont les mêmes, alors pourquoi répéter pour chaque constructeur? Ceci est conforme à l'instruction kokos, mais peut ne pas être lié aux paramètres. Supposons, par exemple, que vous souhaitiez conserver un indicateur qui indique comment l'objet a été créé. Ensuite, cet indicateur serait initialisé différemment pour différents constructeurs quels que soient les paramètres du constructeur. D'un autre côté, si vous répétez la même initialisation pour chaque constructeur, vous laissez la possibilité que vous modifiez (involontairement) le paramètre d'initialisation dans certains constructeurs mais pas dans d'autres. Ainsi, le concept de base ici est que le code commun devrait avoir un emplacement commun et ne pas être potentiellement répété à différents emplacements.

Programmeur Joe
la source
1

Il y a un léger avantage en termes de performances à définir la valeur dans la déclaration. Si vous le définissez dans le constructeur, il est en réalité défini deux fois (d'abord à la valeur par défaut, puis réinitialisé dans le ctor).

John Meagher
la source
2
En C #, les champs sont toujours définis en premier sur la valeur par défaut. La présence d'un initialiseur ne fait aucune différence.
Jeffrey L Whitledge
0

J'essaye normalement le constructeur pour ne rien faire mais obtenir les dépendances et initialiser les membres d'instance liés avec eux. Cela vous rendra la vie plus facile si vous voulez tester vos classes à l'unité.

Si la valeur que vous allez affecter à une variable d'instance n'est influencée par aucun des paramètres que vous allez transmettre à votre constructeur, affectez-la au moment de la déclaration.

Iker Jimenez
la source
0

Pas une réponse directe à votre question sur la meilleure pratique mais un point de mise à jour important et connexe est que dans le cas d'une définition de classe générique, laissez-le sur le compilateur pour initialiser avec les valeurs par défaut ou nous devons utiliser une méthode spéciale pour initialiser les champs à leurs valeurs par défaut (si cela est absolument nécessaire pour la lisibilité du code).

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
    }
}

Et la méthode spéciale pour initialiser un champ générique à sa valeur par défaut est la suivante:

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

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

Lorsque vous n'avez pas besoin de logique ou de gestion des erreurs:

  • Initialiser les champs de classe lors de la déclaration

Lorsque vous avez besoin de logique ou de gestion des erreurs:

  • Initialiser les champs de classe dans le constructeur

Cela fonctionne bien lorsque la valeur d'initialisation est disponible et que l'initialisation peut être placée sur une seule ligne. Cependant, cette forme d'initialisation présente des limites en raison de sa simplicité. Si l'initialisation nécessite une certaine logique (par exemple, la gestion des erreurs ou une boucle for pour remplir un tableau complexe), l'affectation simple est inadéquate. Les variables d'instance peuvent être initialisées dans des constructeurs, où la gestion des erreurs ou une autre logique peut être utilisée.

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

Yousha Aleayoub
la source