J'ai une étrange habitude semble-t-il ... d'après mon collègue du 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, fondamentalement, 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 aucune propriété qui n'est utilisée nulle part.
ETA: La raison pour laquelle j'ai fait cela est que ma classe a plusieurs propriétés qui renvoient une instance d'une autre classe, qui à son tour a également des propriétés avec encore plus de classes, et ainsi de suite. L'appel du constructeur pour la classe supérieure appellerait ensuite tous les constructeurs pour toutes ces classes, quand ils ne sont pas toujours tous nécessaires.
Y a-t-il des objections contre cette pratique, autres que la préférence personnelle?
MISE À JOUR: J'ai examiné les nombreuses opinions divergentes en ce qui concerne cette question et je maintiendrai ma réponse acceptée. Cependant, je suis maintenant parvenu à une bien meilleure compréhension du concept et je suis en mesure de décider quand et quand l'utiliser.
Les inconvénients:
- Problèmes de sécurité des threads
- Ne pas obéir à une requête "setter" lorsque la valeur transmise est nulle
- Micro-optimisations
- La gestion des exceptions doit avoir lieu dans un constructeur
- Besoin de vérifier la valeur nulle dans le code de la classe
Avantages:
- Micro-optimisations
- Les propriétés ne renvoient jamais null
- Retardez ou évitez de charger des objets "lourds"
La plupart des inconvénients ne s'appliquent pas à ma bibliothèque actuelle, mais je devrais tester pour voir si les "micro-optimisations" optimisent réellement quoi que ce soit.
DERNIÈRE MISE À JOUR:
D'accord, j'ai changé ma réponse. Ma question initiale était de savoir si c'était une bonne habitude. Et je suis maintenant convaincu que ce n'est pas le cas. Peut-être que je vais encore l'utiliser dans certaines parties de mon code actuel, mais pas inconditionnellement 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!
la source
Réponses:
Ce que vous avez ici est une implémentation - naïve - de "l'initialisation paresseuse".
Réponse courte:
L'utilisation inconditionnelle de l' initialisation paresseuse n'est pas une bonne idée. Elle a sa place mais il faut prendre en compte les impacts de cette solution.
Contexte et explication:
Mise en œuvre concrète:
regardons d'abord votre échantillon concret et pourquoi je considère sa mise en œuvre naïve:
Il viole le principe de la moindre surprise (POLS) . Lorsqu'une valeur est affectée à une propriété, on s'attend à ce que cette valeur soit renvoyée. Dans votre implémentation, ce n'est pas le cas pour
null
:foo.Bar
sur différents threads peuvent potentiellement obtenir deux instances différentes deBar
et l'un d'eux sera sans connexion à l'Foo
instance. Toutes les modifications apportées à cetteBar
instance sont perdues silencieusement.Ceci est un autre cas de violation du POLS. Lorsque seule la valeur stockée d'une propriété est accédée, elle est censée être thread-safe. Bien que vous puissiez affirmer que la classe n'est tout simplement pas sûre pour les threads - y compris le getter de votre propriété - vous devrez le documenter correctement car ce n'est pas le cas normal. De plus, l'introduction de ce numéro n'est pas nécessaire comme nous le verrons sous peu.
En général:
Il est maintenant temps de regarder l'initialisation paresseuse en général: l'
initialisation paresseuse est généralement utilisée pour retarder la construction d'objets qui prennent beaucoup de temps à être construits ou qui prennent beaucoup de mémoire une fois complètement construits.
C'est une raison très valable pour l'utilisation de l'initialisation paresseuse.
Cependant, ces propriétés n'ont normalement pas de setters, ce qui élimine le premier problème souligné ci-dessus.
De plus, 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 l'implémentation d'une propriété paresseuse, les points suivants sont des problèmes généraux de ce modèle:
La construction de l'objet peut échouer, ce qui entraîne une exception à partir d'un getter de propriété. Ceci est une autre violation du POLS et doit donc être évité. Même la section sur les propriétés dans les «Directives 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 lever d'exceptions:
Les optimisations automatiques par le compilateur sont blessées, à savoir la prédiction d'inlining et de branche. Veuillez consulter la réponse de Bill K pour une explication détaillée.
La conclusion de ces points est la suivante:
Pour chaque propriété unique qui est implémentée paresseusement, vous devriez avoir considéré ces points.
Cela signifie qu'il s'agit d'une décision par cas et ne peut pas être considérée comme une meilleure pratique générale.
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 exposées ci-dessus.
Dans cette section, je veux discuter de certains des points que d'autres ont avancés comme arguments pour l'utilisation inconditionnelle de l'initialisation paresseuse:
Sérialisation:
EricJ déclare dans un commentaire:
Il y a plusieurs problèmes avec cet argument:
Micro-optimisation: votre argument principal est que vous ne voulez construire les objets que lorsque quelqu'un y accède réellement. Vous parlez donc d’optimisation de l’utilisation de la mémoire.
Je ne suis pas d'accord avec cet argument pour les raisons suivantes:
Je reconnais que parfois ce type d'optimisation est justifié. Mais même dans ces cas, l'initialisation paresseuse ne semble pas être la bonne solution. Il y a deux raisons qui s'y opposent:
la source
null
affaire est devenu beaucoup plus fort. :-)C'est un bon choix de conception. Fortement recommandé pour le code de bibliothèque ou les classes principales.
Il est appelé par une certaine «initialisation paresseuse» ou «initialisation retardée» et il est généralement considéré par tous comme un bon choix de conception.
Premièrement, si vous initialisez dans la déclaration des variables de niveau de classe ou du constructeur, alors lorsque votre objet est construit, vous avez la charge de créer une ressource qui ne peut jamais être utilisée.
Deuxièmement, la ressource n'est créée que si nécessaire.
Troisièmement, vous évitez de garbage collection un objet qui n'a pas été utilisé.
Enfin, il est plus facile de gérer les exceptions d'initialisation qui peuvent survenir dans la propriété que les exceptions qui se produisent lors de l'initialisation des variables de niveau classe ou du constructeur.
Il existe des exceptions à cette règle.
Concernant l'argument de performance de la vérification supplémentaire d'initialisation dans la propriété "get", il est insignifiant. L'initialisation et la suppression d'un objet est un impact de performance plus important qu'une simple vérification de pointeur nul avec un saut.
Directives de conception pour le développement de bibliothèques de classes à l' adresse http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
En ce qui concerne
Lazy<T>
La
Lazy<T>
classe générique a été créée exactement pour ce que l'affiche veut, voir Initialisation différée à http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx . Si vous disposez 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 pour faciliter l'implémentation du modèle. De plus, si votre implémentation nécessite la sécurité des threads, vous devez l'ajouter.Types de données primitifs et classes simples
Évidemment, vous n'allez pas utiliser l'initialisation paresseuse pour un type de données primitif ou une utilisation de classe simple comme
List<string>
.Avant de commenter sur Lazy
Lazy<T>
a été introduit dans .NET 4.0, veuillez donc ne pas ajouter encore un autre commentaire concernant cette classe.Avant de commenter les micro-optimisations
Lorsque vous créez des bibliothèques, vous devez prendre en compte toutes les optimisations. Par exemple, dans les classes .NET, vous verrez des tableaux de bits utilisés pour les variables de classe booléenne tout au long du code afin de réduire la consommation de mémoire et la fragmentation de la mémoire, pour ne citer que deux "micro-optimisations".
Concernant les interfaces utilisateur
Vous n'allez pas utiliser l'initialisation différée pour les classes qui sont directement utilisées par l'interface utilisateur. La semaine dernière, j'ai passé la majeure partie de la journée à supprimer le chargement paresseux de huit collections utilisées dans un modèle de vue pour les combo-box. J'ai un
LookupManager
qui gère le chargement paresseux et la mise en cache des collections nécessaires à tout élément de l'interface utilisateur."Setters"
Je n'ai jamais utilisé une propriété set ("setters") pour une propriété chargée paresseusement. Par conséquent, vous ne permettriez jamais
foo.Bar = null;
. Si vous devez définir,Bar
je créerais une méthode appeléeSetBar(Bar value)
et n'utiliserais pas l'initialisation paresseuseLes collections
Les propriétés de la collection de classes sont toujours initialisées lorsqu'elles sont déclarées car elles ne doivent jamais être nulles.
Classes complexes
Permettez-moi de répéter cela différemment, vous utilisez l'initialisation paresseuse pour les classes complexes. Qui 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.
la source
Envisagez-vous de mettre en œuvre un tel modèle en utilisant
Lazy<T>
?En plus de la création facile d'objets chargés différés, vous bénéficiez de la sécurité des threads pendant l'initialisation de l'objet:
Comme d'autres l'ont dit, vous chargez paresseusement des objets s'ils sont vraiment gourmands en ressources ou si cela prend du temps pour les charger pendant la construction de l'objet.
la source
Lazy<T>
maintenant, et m'abstenir d'utiliser la manière dont j'ai toujours l'habitude de faire.Making the Lazy<T> object thread safe does not protect the lazily initialized object. If multiple threads can access the lazily initialized object, you must make its properties and methods safe for multithreaded access.
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 construction est assez faible, donc cela peut aller dans le constructeur. Mais s'il s'agissait d'une liste pré-remplie, je ne le ferais probablement pas avant d'en avoir besoin pour la première fois.
Fondamentalement, si le coût de la construction dépasse le coût de la vérification conditionnelle de chaque accès, créez-le paresseusement. Sinon, faites-le dans le constructeur.
la source
L'inconvénient que je peux voir est que si vous voulez demander si Bars est nul, cela ne le serait jamais et vous créeriez la liste là-bas.
la source
null
, mais ne le récupérez pas. Normalement, vous supposez que vous récupérez la même valeur que celle que vous mettez dans une propriété.L'instanciation / initialisation paresseuse est un modèle parfaitement viable. Gardez à l'esprit, cependant, qu'en règle générale, les consommateurs de votre API ne s'attendent pas à ce que les getters et les setters prennent du temps perceptible par rapport au PDV de l'utilisateur final (ou échouent).
la source
J'allais juste faire un commentaire sur la réponse de Daniel, mais honnêtement, je ne pense pas que cela va assez loin.
Bien que ce soit 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'un des avantages d'un objet est qu'il offre un environnement sécurisé et fiable. Le meilleur cas est si vous créez autant de champs que possible "Final", en les remplissant tous avec le constructeur. Cela rend votre classe assez à l'épreuve des balles. Permettre aux champs d'être modifiés par le biais des setters est un peu moins le cas, mais pas terrible. Par exemple:
Avec votre modèle, la méthode toString ressemblerait à ceci:
Non seulement cela, mais vous avez besoin de vérifications nulles partout où vous pourriez éventuellement utiliser cet objet dans votre classe (en dehors de votre classe, c'est sûr en raison de la vérification nulle dans le getter, mais vous devriez principalement utiliser les membres de vos classes à l'intérieur de la classe)
De plus, votre classe est perpétuellement dans un état incertain - par exemple, si vous décidiez de faire de cette classe une classe d'hibernation en ajoutant quelques annotations, comment le feriez-vous?
Si vous prenez une décision basée sur une micro-optomisation sans exigences ni tests, c'est presque certainement 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 provoquer un échec de prédiction de branche sur le processeur, ce qui ralentira les choses beaucoup plus de fois que attribuer simplement une valeur dans le constructeur, sauf si l'objet que vous créez est assez 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 reprises, pas 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é?
la source
toString()
appelleraitgetName()
, ne pas utilisername
directement.Permettez-moi d'ajouter encore 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'
Bar
exécution du code , ce qui pourrait potentiellement instancier le plus tôt possible en exécutant simplement le code. En d'autres termes, le simple fait de déboguer modifie l'exécution du programme.Cela peut être un problème ou non (en fonction des effets secondaires), mais il faut en être conscient.
la source
Êtes-vous sûr que Foo devrait instancier quoi que ce soit?
Pour moi, cela semble malodorant (mais pas nécessairement faux ) de laisser Foo instancier quoi que ce soit. À moins que ce soit le but exprès de Foo d'être une usine, il ne devrait pas instancier ses propres collaborateurs, mais plutôt les injecter dans son constructeur .
Si cependant le but de Foo est de créer des instances de type Bar, alors je ne vois rien de mal à le faire paresseusement.
la source