Quand dois-je utiliser Lazy <T>?

327

J'ai trouvé cet article sur Lazy: La paresse en C # 4.0 - Lazy

Quelle est la meilleure pratique pour obtenir les meilleures performances en utilisant des objets Lazy? Quelqu'un peut-il m'indiquer une utilisation pratique dans une application réelle? En d'autres termes, quand dois-je l'utiliser?

danyolgiax
la source
42
REMPLACE: get { if (foo == null) foo = new Foo(); return foo; }. Et il y a des millions d'endroits possibles pour l'utiliser ...
Kirk Woll
57
Notez que ce get { if (foo == null) foo = new Foo(); return foo; }n'est pas thread-safe, tandis que Lazy<T>thread-safe par défaut.
Matthew
23
Depuis MSDN: IMPORTANT: l'initialisation paresseuse est thread-safe, mais elle ne protège pas l'objet après sa création. Vous devez verrouiller l'objet avant d'y accéder, sauf si le type est thread-safe.
Pedro.The.Kid

Réponses:

237

Vous l'utilisez généralement lorsque vous souhaitez instancier quelque chose la première fois qu'il est réellement utilisé. Cela retarde le coût de sa création jusqu'à si / quand il est nécessaire au lieu de toujours encourir le coût.

Habituellement, cela est préférable lorsque l'objet peut être utilisé ou non et que le coût de sa construction n'est pas négligeable.

James Michael Hare
la source
121
pourquoi ne pas utiliser TOUJOURS Lazy?
TruthOf42
44
Cela entraîne des coûts lors de la première utilisation et peut utiliser une surcharge de verrouillage (ou sacrifie la sécurité du fil sinon) pour ce faire. Ainsi, il doit être choisi avec soin et ne doit être utilisé qu'en cas de besoin.
James Michael Hare
3
James pourriez-vous s'il vous plaît développer "et le coût de construction n'est pas anodin"? Dans mon cas, j'ai 19 propriétés dans ma classe et dans la plupart des cas, seules 2 ou 3 devront être examinées. Par conséquent, j'envisage de mettre en œuvre chaque propriété à l'aide Lazy<T>. Cependant, pour créer chaque propriété, je fais une interpolation linéaire (ou une interpolation bilinéaire) qui est assez triviale mais a un certain coût. (Allez-vous suggérer que j'aille faire ma propre expérience?)
Ben
3
James, suivant mes propres conseils, j'ai fait ma propre expérience. Voir mon post .
Ben
17
Vous voudrez peut-être tout initialiser / instancier «pendant» le démarrage du système pour éviter la latence des utilisateurs dans les systèmes à haut débit et à faible latence. Ce n'est là qu'une des nombreuses raisons de ne pas utiliser "toujours" Lazy.
Derrick
126

Vous devriez essayer d'éviter d'utiliser des singletons, mais si jamais vous en avez besoin, cela Lazy<T>facilite l'implémentation de singletons paresseux et thread-safe:

public sealed class Singleton
{
    // Because Singleton's constructor is private, we must explicitly
    // give the Lazy<Singleton> a delegate for creating the Singleton.
    static readonly Lazy<Singleton> instanceHolder =
        new Lazy<Singleton>(() => new Singleton());

    Singleton()
    {
        // Explicit private constructor to prevent default public constructor.
        ...
    }

    public static Singleton Instance => instanceHolder.Value;
}
Matthieu
la source
38
Je déteste lire Vous devriez essayer d'éviter d'utiliser des singletons lorsque je les utilise: D ... maintenant je dois savoir pourquoi je devrais essayer de les éviter: D
Bart Calixto
24
J'arrêterai d'utiliser Singletons lorsque Microsoft cessera de les utiliser dans leurs exemples.
eaglei22
4
J'ai tendance à être en désaccord avec l'idée de devoir éviter les singletons. Lorsque vous suivez le paradigme de l'injection de dépendance, cela ne devrait pas avoir d'importance dans les deux cas. Idéalement, toutes vos dépendances ne devraient être créées qu'une seule fois. Cela réduit la pression sur le CPG dans les scénarios de charge élevée. Par conséquent, en faire un Singleton à partir de la classe elle-même est très bien. La plupart (sinon la totalité) des conteneurs DI modernes peuvent le gérer comme vous le souhaitez.
Lee Grissom
1
Vous n'avez pas besoin d'utiliser un modèle singleton comme celui-ci, utilisez plutôt n'importe quel conteneur di pour configurer votre classe pour singleton. Le conteneur prendra soin des frais généraux pour vous.
VivekDev
Tout a un but, il y a des situations où les singletons sont une bonne approche et des situations où ce n'est pas le cas :).
Hawkzey
86

Un excellent exemple du monde réel où le chargement paresseux est utile est avec les ORM (Object Relation Mappers) tels que Entity Framework et NHibernate.

Supposons que vous ayez une entité Client qui possède des propriétés pour le nom, le numéro de téléphone et les commandes. Name et PhoneNumber sont des chaînes régulières, mais Orders est une propriété de navigation qui renvoie une liste de toutes les commandes que le client a passées.

Vous voudrez souvent passer par tous vos clients et obtenir leur nom et numéro de téléphone pour les appeler. Il s'agit d'une tâche très rapide et simple, mais imaginez que chaque fois que vous créez un client, il passe automatiquement et effectue une jointure complexe pour renvoyer des milliers de commandes. Le pire, c'est que vous n'allez même pas utiliser les commandes, c'est donc un gaspillage complet de ressources!

C'est l'endroit parfait pour le chargement paresseux, car si la propriété Order est paresseuse, elle n'ira pas chercher toute la commande du client, sauf si vous en avez réellement besoin. Vous pouvez énumérer les objets Client en obtenant uniquement leur nom et leur numéro de téléphone pendant que la propriété Order dort patiemment, prête lorsque vous en avez besoin.

Despertar
la source
34
Mauvais exemple, car un tel chargement paresseux est généralement déjà intégré à l'ORM. Vous ne devriez pas commencer à ajouter des valeurs Lazy <T> à vos POCO pour obtenir un chargement paresseux, mais utilisez la méthode spécifique à ORM pour le faire.
Dynalon
56
@Dyna Cet exemple fait référence au chargement paresseux intégré d'un ORM car je pense que cela illustre l'utilité du chargement paresseux de manière claire et simple.
Despertar
Donc, si vous utilisez Entity Framework, faut-il appliquer leur propre paresseux? Ou EF le fait-il pour vous?
Zapnologica
7
@Zapnologica EF fait tout cela pour vous par défaut. En fait, si vous voulez un chargement rapide (l'opposé du chargement paresseux), vous devez le dire explicitement à EF en utilisant Db.Customers.Include("Orders"). Cela entraînera l'exécution de la jointure d'ordre à ce moment plutôt qu'à la Customer.Orderspremière utilisation de la propriété. Le chargement différé peut également être désactivé via DbContext.
Despertar
2
En fait, c'est un bon exemple, car on peut vouloir ajouter cette fonctionnalité lors de l'utilisation de quelque chose comme Dapper.
tbone
41

J'ai envisagé d'utiliser des Lazy<T>propriétés pour améliorer les performances de mon propre code (et pour en savoir un peu plus). Je suis venu ici pour chercher des réponses sur le moment de l'utiliser, mais il semble que partout où je vais, il y a des phrases comme:

Utilisez l'initialisation paresseuse pour différer la création d'un objet volumineux ou gourmand en ressources, ou l'exécution d'une tâche gourmande en ressources, en particulier lorsqu'une telle création ou exécution risque de ne pas se produire pendant la durée de vie du programme.

de MSDN Lazy <T> Class

Je suis un peu confus car je ne sais pas où tracer la ligne. Par exemple, je considère l'interpolation linéaire comme un calcul assez rapide, mais si je n'ai pas besoin de le faire, l'initialisation paresseuse peut-elle m'aider à éviter de le faire et cela en vaut-il la peine?

Finalement, j'ai décidé d'essayer mon propre test et j'ai pensé partager les résultats ici. Malheureusement, je ne suis pas vraiment un expert dans ce genre de tests et je suis donc heureux d'obtenir des commentaires suggérant des améliorations.

La description

Pour mon cas, j'étais particulièrement intéressé de voir si Lazy Properties pouvait aider à améliorer une partie de mon code qui fait beaucoup d'interpolation (la plupart étant inutilisée) et j'ai donc créé un test qui comparait 3 approches.

J'ai créé une classe de test distincte avec 20 propriétés de test (appelons-les propriétés t) pour chaque approche.

  • Classe GetInterp: exécute une interpolation linéaire chaque fois qu'une propriété t est obtenue.
  • Classe InitInterp: Initialise les propriétés t en exécutant l'interpolation linéaire pour chacune dans le constructeur. Le get retourne juste un double.
  • Classe InitLazy: définit les propriétés t en tant que propriétés paresseuses afin que l'interpolation linéaire soit exécutée une fois lors de l' obtention de la propriété. Les get suivants devraient simplement renvoyer un double déjà calculé.

Les résultats des tests sont mesurés en ms et correspondent à la moyenne de 50 instanciations ou 20 propriétés obtenues. Chaque test a ensuite été exécuté 5 fois.

Résultats du test 1: Instanciation (moyenne de 50 instanciations)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.005668 0.005722 0.006704 0.006652 0.005572 0.0060636 6.72
InitInterp 0.08481  0.084908 0.099328 0.098626 0.083774 0.0902892 100.00
InitLazy   0.058436 0.05891  0.068046 0.068108 0.060648 0.0628296 69.59

Résultats du test 2: première acquisition (moyenne de 20 propriétés)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.263    0.268725 0.31373  0.263745 0.279675 0.277775 54.38
InitInterp 0.16316  0.161845 0.18675  0.163535 0.173625 0.169783 33.24
InitLazy   0.46932  0.55299  0.54726  0.47878  0.505635 0.510797 100.00

Résultats du test 3: Second Get (moyenne de 20 propriétés)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.08184  0.129325 0.112035 0.097575 0.098695 0.103894 85.30
InitInterp 0.102755 0.128865 0.111335 0.10137  0.106045 0.110074 90.37
InitLazy   0.19603  0.105715 0.107975 0.10034  0.098935 0.121799 100.00

Observations

GetInterpest plus rapide à instancier comme prévu car il ne fait rien. InitLazyest plus rapide à instancier que de InitInterpsuggérer que la surcharge dans la configuration des propriétés paresseuses est plus rapide que mon calcul d'interpolation linéaire. Cependant, je suis un peu confus ici car InitInterpdevrait faire 20 interpolations linéaires (pour configurer ses propriétés t) mais il ne faut que 0,09 ms pour instancier (test 1), par rapport à GetInterpce qui prend 0,28 ms pour effectuer une seule interpolation linéaire la première fois (test 2), et 0,1 ms pour le faire la deuxième fois (test 3).

Cela prend InitLazypresque 2 fois plus de temps que GetInterpd'obtenir une propriété la première fois, tandis que InitInterpc'est le plus rapide, car il a rempli ses propriétés lors de l'instanciation. (Au moins, c'est ce qu'il aurait dû faire, mais pourquoi son résultat d'instanciation était-il tellement plus rapide qu'une simple interpolation linéaire? Quand fait-il exactement ces interpolations?)

Malheureusement, il semble qu'il y ait une optimisation automatique du code en cours dans mes tests. Cela devrait prendre GetInterple même temps pour obtenir une propriété la première fois que pour la deuxième fois, mais il apparaît plus de 2 fois plus rapidement. Il semble que cette optimisation affecte également les autres classes car elles prennent toutes à peu près le même temps pour le test 3. Cependant, de telles optimisations peuvent également avoir lieu dans mon propre code de production, ce qui peut également être une considération importante.

Conclusions

Bien que certains résultats soient conformes aux attentes, il existe également des résultats inattendus très intéressants probablement dus à des optimisations de code. Même pour les classes qui semblent faire beaucoup de travail dans le constructeur, les résultats de l'instanciation montrent qu'ils peuvent toujours être très rapides à créer, par rapport à l'obtention d'une double propriété. Bien que les experts dans ce domaine puissent commenter et enquêter de manière plus approfondie, mon sentiment personnel est que je dois refaire ce test mais sur mon code de production afin d'examiner quel type d'optimisations peuvent également y avoir lieu. Cependant, je m'attends à ce que ce InitInterpsoit la voie à suivre.

Ben
la source
26
vous devriez peut-être publier votre code de test pour reproduire votre sortie, car sans connaître votre code, il sera difficile de suggérer quoi que ce soit
WiiMaxx
1
Je crois que le principal compromis est entre l'utilisation de la mémoire (paresseuse) et l'utilisation du processeur (non paresseuse). Parce que lazydoit faire une comptabilité supplémentaire, InitLazyutiliserait plus de mémoire que les autres solutions. Il peut également avoir un impact mineur sur les performances de chaque accès, tout en vérifiant s'il a déjà une valeur ou non; des astuces intelligentes pourraient supprimer cette surcharge, mais cela nécessiterait un support spécial en IL. (Haskell le fait en faisant de chaque valeur paresseuse un appel de fonction; une fois la valeur générée, elle est remplacée par une fonction qui renvoie cette valeur à chaque fois.)
jpaugh
14

Juste pour montrer l'exemple affiché par Mathew

public sealed class Singleton
{
    // Because Singleton's constructor is private, we must explicitly
    // give the Lazy<Singleton> a delegate for creating the Singleton.
    private static readonly Lazy<Singleton> instanceHolder =
        new Lazy<Singleton>(() => new Singleton());

    private Singleton()
    {
        ...
    }

    public static Singleton Instance
    {
        get { return instanceHolder.Value; }
    }
}

avant la naissance du Lazy, nous l'aurions fait de cette façon:

private static object lockingObject = new object();
public static LazySample InstanceCreation()
{
    if(lazilyInitObject == null)
    {
         lock (lockingObject)
         {
              if(lazilyInitObject == null)
              {
                   lazilyInitObject = new LazySample ();
              }
         }
    }
    return lazilyInitObject ;
}
Thulani Chivandikwa
la source
6
J'utilise toujours un conteneur IoC pour cela.
Jowen
1
Je suis tout à fait d'accord pour envisager un conteneur IoC pour cela. Si toutefois vous voulez un singleton d'objet initialisé paresseux simple, considérez également que si vous n'en avez pas besoin pour être sûr pour les threads, le faire manuellement avec un If peut être préférable compte tenu de la surcharge de performances de la façon dont Lazy se gère lui-même.
Thulani Chivandikwa
12

Depuis MSDN:

Utilisez une instance de Lazy pour différer la création d'un objet volumineux ou gourmand en ressources ou l'exécution d'une tâche gourmande en ressources, en particulier lorsqu'une telle création ou exécution risque de ne pas se produire pendant la durée de vie du programme.

En plus de la réponse de James Michael Hare, Lazy fournit une initialisation thread-safe de votre valeur. Jetez un œil à l' entrée MSDN de l'énumération LazyThreadSafetyMode décrivant divers types de modes de sécurité des threads pour cette classe.

Vasea
la source
-2

Vous devriez regarder cet exemple pour comprendre l'architecture de chargement paresseux

private readonly Lazy<List<int>> list = new Lazy<List<int>>(() =>
{
    List<int> configList = new List<int>(Thread.CurrentThread.ManagedThreadId);
    return configList;
});
public void Execute()
{
    list.Value.Add(0);
    if (list.IsValueCreated)
    {
        list.Value.Add(1);
        list.Value.Add(2);

        foreach (var item in list.Value)
        {
            Console.WriteLine(item);
        }
    }
    else
    {
        Console.WriteLine("Value not created");
    }
}

-> sortie -> 0 1 2

mais si ce code n'écrit pas "list.Value.Add (0);"

sortie -> Valeur non créée

Tufy Duck
la source