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:
Avantages:
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!
Ce que vous avez ici est une implémentation "naïve" de "l'initialisation paresseuse".
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.
Mise en oeuvre concrète:
Voyons d’abord votre exemple concret et pourquoi je considère sa mise en œuvre naïve:
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
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. 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:
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.
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:
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:
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:
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:
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
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.
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>
.
Lazy<T>
A été introduit dans .NET 4.0, n’ajoutez donc pas un autre commentaire à propos de cette classe.
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".
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.
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.
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.
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.
Je n'ai jamais dit de faire cela pour toutes les classes ou dans tous les cas. C'est une mauvaise habitude.
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.
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.
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.
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é?
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).
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.
Ê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.