web-dev-qa-db-fra.com

Bonne ou mauvaise pratique? Initialisation d'objets dans le getter

J'ai une étrange habitude, semble-t-il ... d'après ma collègue au moins. Nous travaillons ensemble sur un petit projet. La façon dont j'ai écrit les classes est (exemple simplifié):

[Serializable()]
public class Foo
{
    public Foo()
    { }

    private Bar _bar;

    public Bar Bar
    {
        get
        {
            if (_bar == null)
                _bar = new Bar();

            return _bar;
        }
        set { _bar = value; }
    }
}

Donc, en gros, je n’initialise un champ que lorsqu'un getter est appelé et que le champ est toujours nul. J'ai pensé que cela réduirait la surcharge en n'initialisant pas les propriétés qui ne sont utilisées nulle part.

ETA: C'est parce que ma classe a plusieurs propriétés qui renvoient une instance d'une autre classe, qui à leur tour ont aussi des propriétés avec encore plus de classes, etc. L'appel du constructeur de la classe supérieure appellera ensuite tous les constructeurs de toutes ces classes, quand ils ne sont pas toujours tous nécessaires.

Y at-il des objections à cette pratique, autres que la préférence personnelle?

MISE À JOUR: J'ai examiné les nombreuses opinions divergentes sur cette question et je maintiendrai ma réponse acceptée. Cependant, je comprends maintenant beaucoup mieux le concept et je suis en mesure de décider quand et quand l’utiliser.

Les inconvénients:

  • Problèmes de sécurité du fil
  • Ne pas obéir à une requête "setter" lorsque la valeur transmise est null
  • Micro-optimisations
  • La gestion des exceptions doit avoir lieu dans un constructeur
  • Besoin de vérifier la valeur null dans le code de la classe

Avantages:

  • Micro-optimisations
  • Les propriétés ne retournent jamais null
  • Retardez ou évitez de charger des objets "lourds"

La plupart des inconvénients ne sont pas applicables à ma bibliothèque actuelle, cependant, je devrais vérifier si les "micro-optimisations" optimisent réellement quoi que ce soit.

DERNIÈRE MISE À JOUR:

Ok, j'ai changé ma réponse. Ma question initiale était de savoir s'il s'agit ou non d'une bonne habitude. Et je suis maintenant convaincu que ce n'est pas. Peut-être que je continuerai à l'utiliser dans certaines parties de mon code actuel, mais pas de manière inconditionnelle et certainement pas tout le temps. Je vais donc perdre mon habitude et y réfléchir avant de l'utiliser. Merci tout le monde!

166
John Willemse

Ce que vous avez ici est une implémentation "naïve" de "l'initialisation paresseuse".

Réponse courte:

Utiliser l'initialisation paresseuse inconditionnellement n'est pas une bonne idée. Il a sa place mais il faut prendre en compte les impacts de cette solution.

Contexte et explication:

Mise en oeuvre concrète:
Voyons d’abord votre exemple concret et pourquoi je considère sa mise en œuvre naïve:

  1. Il enfreint le principe principe de moindre surprise (POLS) . Lorsqu'une valeur est affectée à une propriété, cette valeur est attendue. Dans votre implémentation, ce n'est pas le cas pour null:

    foo.Bar = null;
    Assert.Null(foo.Bar); // This will fail
    
  2. Il introduit quelques problèmes de thread: Deux appelants de foo.Bar sur des threads différents peut potentiellement obtenir deux instances différentes de Bar et l'une d'elles sera sans connexion à l'instance Foo. Toute modification apportée à cette instance Bar est automatiquement perdue.
    Ceci est un autre cas de violation de POLS. Lorsque seul l'accès à la valeur stockée d'une propriété est utilisée, elle est censée être thread-safe. Bien que vous puissiez argumenter que la classe n'est tout simplement pas thread-safe - y compris le getter de votre propriété -, vous devrez documenter cela correctement car ce n'est pas le cas normal. De plus, l'introduction de cette question est inutile, comme nous le verrons bientôt.

En général:
Il est maintenant temps de regarder l’initialisation paresseuse en général:
L’initialisation lente est généralement utilisée pour retarder la construction d’objets dont la construction prend beaucoup de temps ou qui prend beaucoup de mémoire une fois complètement construite.
C’est une raison très valable d’utiliser l’initialisation différée.

Cependant, ces propriétés ne possèdent normalement pas de setters, ce qui élimine le premier problème mentionné ci-dessus.
En outre, une implémentation thread-safe serait utilisée - comme Lazy<T> - pour éviter le deuxième problème.

Même en considérant ces deux points dans la mise en œuvre d'une propriété paresseuse, les points suivants sont des problèmes généraux de ce modèle:

  1. La construction de l'objet peut échouer, ce qui entraîne une exception pour un acquéreur de propriété. Ceci est encore une autre violation de POLS et doit donc être évité. Même le section sur les propriétés dans les "Instructions de conception pour le développement de bibliothèques de classes" indique explicitement que les récupérateurs de propriétés ne doivent pas renvoyer d'exceptions:

    Évitez de lancer des exceptions à partir de getters de propriétés.

    Les getters de propriétés doivent être des opérations simples, sans aucune condition préalable. Si un getter peut générer une exception, envisagez de redéfinir la propriété en tant que méthode.

  2. Les optimisations automatiques effectuées par le compilateur sont endommagées, à savoir l'inclusion et la prédiction de branches. Veuillez voir réponse de Bill K pour une explication détaillée.

La conclusion de ces points est la suivante:
Pour chaque propriété implémentée paresseusement, vous devriez avoir pris en compte ces points.
Cela signifie qu’il s’agit d’une décision prise au cas par cas et ne peut être considérée comme une pratique exemplaire.

Ce modèle a sa place, mais ce n'est pas une bonne pratique générale lors de l'implémentation de classes. Il ne doit pas être utilisé sans condition , pour les raisons indiquées ci-dessus.


Dans cette section, je souhaite aborder certains des points soulevés par d’autres comme étant des arguments en faveur de l’initialisation paresseuse sans condition:

  1. Sérialisation:
    EricJ déclare dans un commentaire:

    Un objet qui peut être sérialisé n'aura pas son constructeur appelé lors de la désérialisation (dépend du sérialiseur, mais de nombreux courants se comportent de la sorte). Si vous insérez le code d'initialisation dans le constructeur, vous devez fournir un support supplémentaire pour la désérialisation. Ce modèle évite ce codage spécial.

    Cet argument pose plusieurs problèmes:

    1. La plupart des objets ne seront jamais sérialisés. Ajouter une sorte de support pour cela quand ce n'est pas nécessaire viole YAGNI .
    2. Lorsqu'une classe doit prendre en charge la sérialisation, il existe des moyens de l'activer sans solution de contournement qui n'a rien à voir avec la sérialisation à première vue.
  2. Micro-optimisation: Votre principal argument est que vous ne voulez construire les objets que lorsque quelqu'un y accède. Vous parlez donc d'optimiser l'utilisation de la mémoire.
    Je ne souscris pas à cet argument pour les raisons suivantes:

    1. Dans la plupart des cas, quelques objets en mémoire n'ont aucun impact sur quoi que ce soit. Les ordinateurs modernes ont suffisamment de mémoire. Sans un cas de problèmes réels confirmés par un profileur, il s'agit de optimisation pré-mature et il y a de bonnes raisons de le contrer.
    2. Je reconnais que parfois ce type d’optimisation est justifié. Mais même dans ces cas, une initialisation lente ne semble pas être la solution correcte. Deux raisons s’opposent à cela:

      1. Une initialisation lente peut potentiellement nuire aux performances. Peut-être seulement de manière marginale, mais comme l'a montré la réponse de Bill, l'impact est plus important qu'on pourrait le penser à première vue. Donc, cette approche traite essentiellement la performance par rapport à la mémoire.
      2. Si vous utilisez un dessin dont le cas d'utilisation courant consiste à n'utiliser que des parties de la classe, cela suggère un problème avec le dessin lui-même: La classe en question a probablement plusieurs responsabilités. La solution serait de scinder la classe en plusieurs classes plus ciblées.
170
Daniel Hilgarth

C'est un bon choix de conception. Fortement recommandé pour le code de bibliothèque ou les classes principales.

Il est appelé "initialisation différée" ou "initialisation différée" et est généralement considéré par tous comme un bon choix de conception.

Premièrement, si vous initialisez dans la déclaration de variables de niveau de classe ou de constructeur, une fois votre objet construit, vous aurez le temps de créer une ressource qui ne pourra jamais être utilisée.

Deuxièmement, la ressource est créée uniquement si nécessaire.

Troisièmement, vous évitez de ramasser des ordures dans un objet non utilisé.

Enfin, il est plus facile de gérer les exceptions d'initialisation pouvant se produire dans la propriété, puis les exceptions se produisant lors de l'initialisation de variables de niveau classe ou du constructeur.

Il y a des exceptions à cette règle.

En ce qui concerne l'argument de performance de la vérification supplémentaire pour l'initialisation dans la propriété "get", il est non significatif. L’initialisation et la suppression d’un objet est un résultat beaucoup plus important qu’une simple vérification du pointeur nul avec saut.

Instructions de conception pour le développement de bibliothèques de classes à l'adresse http://msdn.Microsoft.com/en-US/library/vstudio/ms229042.aspx

Concernant Lazy<T>

La classe générique Lazy<T> A été créée exactement pour répondre aux besoins de l’affiche, voir Initialisation différée à l’adresse http://msdn.Microsoft.com/en-us/library /dd997286(v=vs.100).aspx . Si vous avez d'anciennes versions de .NET, vous devez utiliser le modèle de code illustré dans la question. Ce modèle de code est devenu si courant que Microsoft a jugé bon d’inclure une classe dans les dernières bibliothèques .NET afin de faciliter l’implémentation du modèle. De plus, si votre implémentation requiert la sécurité des threads, vous devez l'ajouter.

Types de données primitifs et classes simples

Evidemment, vous n'allez pas utiliser l'initialisation différée pour un type de données primitif ou une utilisation de classe simple comme List<string>.

Avant de parler de Lazy

Lazy<T> A été introduit dans .NET 4.0, n’ajoutez donc pas un autre commentaire à propos de cette classe.

Avant de commenter les micro-optimisations

Lorsque vous construisez des bibliothèques, vous devez prendre en compte toutes les optimisations. Par exemple, dans les classes .NET, vous verrez les tableaux de bits utilisés pour les variables de classe booléennes dans le code afin de réduire la consommation de mémoire et la fragmentation de la mémoire, pour ne nommer que deux "micro-optimisations".

Concernant les interfaces utilisateur

Vous n'allez pas utiliser l'initialisation différée pour les classes directement utilisées par l'interface utilisateur. La semaine dernière, j'ai passé la majeure partie de la journée à éliminer le chargement paresseux de huit collections utilisées dans un modèle de vue pour les boîtes à options. J'ai un LookupManager qui gère le chargement paresseux et la mise en cache des collections nécessaires à tout élément d'interface utilisateur.

"Setters"

Je n'ai jamais utilisé une propriété set ("setters") pour une propriété chargée paresseuse. Par conséquent, vous ne permettrez jamais foo.Bar = null;. Si vous avez besoin de définir Bar, je créerais une méthode appelée SetBar(Bar value) et n'utiliserais pas d'initialisation différée.

Des collections

Les propriétés de collection de classes sont toujours initialisées lorsqu'elles sont déclarées car elles ne doivent jamais être null.

Classes Complexes

Permettez-moi de répéter ceci différemment, vous utilisez l'initialisation différée pour les classes complexes. Ce sont généralement des classes mal conçues.

Enfin

Je n'ai jamais dit de faire cela pour toutes les classes ou dans tous les cas. C'est une mauvaise habitude.

49
AMissico

Envisagez-vous de mettre en œuvre un tel motif en utilisant Lazy<T>?

En plus de la création facile d'objets chargés paresseux, vous bénéficiez d'une sécurité des threads pendant l'initialisation de l'objet:

Comme d'autres l'ont dit, vous chargez des objets paresseusement s'ils sont très gourmands en ressources ou si cela prend un certain temps pour les charger pendant la construction de l'objet.

17
Matías Fidemraizer

L'inconvénient que je peux voir est que si vous voulez demander si Bars est nul, cela ne le sera jamais et vous créeriez la liste à cet endroit.

9
Luis Tellez

Je pense que cela dépend de ce que vous initialisez. Je ne le ferais probablement pas pour une liste car le coût de la construction est assez petit, donc ça peut aller dans le constructeur. Mais s'il s'agissait d'une liste préremplie, je ne le ferais probablement pas avant que cela soit nécessaire pour la première fois.

Fondamentalement, si le coût de la construction est supérieur au coût d'un contrôle conditionnel de chaque accès, créez-le paresseux. Sinon, faites-le dans le constructeur.

9
Colin Mackay

J'allais juste commenter la réponse de Daniel mais honnêtement, je ne pense pas que cela aille assez loin.

Bien que c’est un très bon modèle à utiliser dans certaines situations (par exemple, lorsque l’objet est initialisé à partir de la base de données), c’est une habitude HORRIBLE à prendre.

L'une des meilleures choses à propos d'un objet est qu'il offre un environnement sécurisé et sécurisé. Le meilleur des cas est si vous faites autant de champs que possible "Final", en les remplissant tous avec le constructeur. Cela rend votre classe assez résistante aux balles. Permettre aux champs d'être modifiés par les colons est un peu moins, mais pas terrible. Par exemple:

 class SafeClass 
 {
 String name = ""; 
 Entier age = 0; 
 
 public void setName (String newName ) 
 {
 assert (newName! = null) 
 name = newName; 
} // suit ce modèle pour l’âge 
 ... 
 public String toString () {
 String s = "La classe de sécurité a pour nom:" + name + "et age:" + age 
} 
} 

Avec votre modèle, la méthode toString ressemblerait à ceci:

 if (nom == null) 
 génère une nouvelle exception IllegalStateException ("SafeClass est entré dans un état illégal! nom est nul") 
 if (age == null) 
 throw new IllegalStateException ("SafeClass est entrée dans un état illégal! age est null") 
 
 public String toString () {
 String s = "La classe de sécurité a pour nom:" + nom + "et âge:" + âge 
} 

Non seulement cela, mais vous avez besoin de contrôles nuls partout où vous pourriez éventuellement utiliser cet objet dans votre classe (en dehors de votre classe est sûr à cause du contrôle nul dans le getter, mais vous devriez utiliser principalement vos membres de classes dans la classe)

De plus, votre classe est constamment dans un état incertain - par exemple, si vous décidez de transformer cette classe en classe d'hibernation en ajoutant quelques annotations, comment le ferez-vous?

Si vous prenez une décision basée sur une micro-optimisation sans exigences ni tests, c'est probablement la mauvaise décision. En fait, il y a de très bonnes chances que votre modèle ralentisse réellement le système, même dans les circonstances les plus idéales, car l'instruction if peut entraîner l'échec de la prédiction d'une branche sur le processeur, ce qui ralentira considérablement les choses. n'affectez qu'une valeur dans le constructeur, sauf si l'objet que vous créez est relativement complexe ou provient d'une source de données distante.

Pour un exemple du problème de prédiction de brance (que vous rencontrez plusieurs fois, une seule fois), voyez la première réponse à cette question géniale: Pourquoi est-il plus rapide de traiter un tableau trié qu'un tableau non trié?

8
Bill K

L'instanciation/initialisation paresseuse est un modèle parfaitement viable. Gardez toutefois à l'esprit qu'en règle générale, les utilisateurs de votre API ne s'attendent pas à ce que les accesseurs prennent une heure discernable à partir du POV de l'utilisateur final (ou échouent).

8
Tormod

Permettez-moi d’ajouter un point à de nombreux points positifs soulevés par d’autres ...

Le débogueur évaluera ( par défaut ) les propriétés lors de l'exploration détaillée du code, ce qui pourrait potentiellement instancier le Bar plus tôt que ne le ferait normalement l'exécution du code. En d'autres termes, le simple fait de déboguer modifie l'exécution du programme.

Cela peut ou peut ne pas être un problème (en fonction des effets secondaires), mais c'est quelque chose à être au courant.

4
Branko Dimitrijevic

Êtes-vous sûr que Foo devrait instancier quoi que ce soit?

Pour moi, cela semble mauvais (mais pas nécessairement mal ) de laisser Foo instancier quoi que ce soit. À moins que le but exprès de Foo soit d'être une usine, il ne devrait pas instancier ses propres collaborateurs, mais au lieu de cela, les faire injecter dans son constructeur .

Si toutefois le but de Foo est de créer des instances de type Bar, alors je ne vois rien de mal à le faire paresseusement.

2
KaptajnKold