¿Cuándo debo usar Lazy <T>?

327

Encontré este artículo sobre Lazy: Pereza en C # 4.0 - Perezoso

¿Cuál es la mejor práctica para tener el mejor rendimiento con objetos perezosos? ¿Alguien puede señalarme un uso práctico en una aplicación real? En otras palabras, ¿cuándo debería usarlo?

danyolgiax
fuente
42
Sustituye a: get { if (foo == null) foo = new Foo(); return foo; }. Y hay millones de lugares posibles para usarlo ...
Kirk Woll
57
Tenga en cuenta que get { if (foo == null) foo = new Foo(); return foo; }no es seguro para subprocesos, mientras que Lazy<T>es seguro para subprocesos de forma predeterminada.
Matthew
23
Desde MSDN: IMPORTANTE: La inicialización diferida es segura para subprocesos, pero no protege el objeto después de la creación. Debe bloquear el objeto antes de acceder a él, a menos que el tipo sea seguro para subprocesos.
Pedro The.Kid

Respuestas:

237

Por lo general, lo usa cuando desea crear una instancia de algo la primera vez que se usa realmente. Esto retrasa el costo de crearlo hasta que sea necesario en lugar de incurrir siempre en el costo.

Por lo general, esto es preferible cuando el objeto puede o no usarse y el costo de construirlo no es trivial.

James Michael Hare
fuente
121
¿Por qué no usar SIEMPRE Lazy?
TruthOf42
44
Incurre en el costo en el primer uso y puede usar algunos gastos generales de bloqueo (o sacrifica la seguridad del hilo si no) para hacerlo. Por lo tanto, debe elegirse con cuidado y no usarse a menos que sea necesario.
James Michael Hare
3
James, ¿podrías ampliar "y el costo de la construcción no es trivial"? En mi caso, tengo 19 propiedades en mi clase y en la mayoría de los casos solo será necesario mirar 2 o 3. Por lo tanto, estoy considerando implementar cada propiedad usando Lazy<T>. Sin embargo, para crear cada propiedad, estoy haciendo una interpolación lineal (o una interpolación bilineal) que es bastante trivial pero tiene algún costo. (¿Vas a sugerirme que vaya y haga mi propio experimento?)
Ben
3
James, siguiendo mi propio consejo, hice mi propio experimento. Mira mi post .
Ben
17
Es posible que desee inicializar / instanciar todo "durante" el inicio del sistema para evitar la latencia del usuario en sistemas de alto rendimiento y baja latencia. Esta es solo una de las muchas razones para no "siempre" usar Lazy.
Derrick
126

Debe intentar evitar el uso de Singletons, pero si alguna vez lo necesita, Lazy<T>facilita la implementación de Singletons perezosos y seguros para subprocesos:

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;
}
Mateo
fuente
38
Odio leer Deberías tratar de evitar usar Singletons cuando los estoy usando: D ... ahora necesito saber por qué debería tratar de evitarlos: D
Bart Calixto
24
Dejaré de usar Singletons cuando Microsoft deje de usarlos en sus ejemplos.
eaglei22
44
Tiendo a estar en desacuerdo con la noción de que necesito evitar Singletons. Al seguir el paradigma de inyección de dependencia, no debería importar de ninguna manera. Idealmente, todas sus dependencias solo deberían crearse una vez. Esto reduce la presión sobre el GC en escenarios de alta carga. Por lo tanto, hacerlos un Singleton desde dentro de la clase está bien. La mayoría (si no todos) los contenedores DI modernos pueden manejarlo de cualquier manera que elija.
Lee Grissom
1
No tiene que usar un patrón singleton como ese, en su lugar use cualquier contenedor di configure su clase para singleton. El contenedor se encargará de los gastos generales por usted.
VivekDev
Todo tiene un propósito, hay situaciones en las que los singletons son un buen enfoque y situaciones en las que no lo es :).
Hawkzey
86

Un gran ejemplo del mundo real de donde la carga diferida es útil es con ORM (Object Relation Mappers) como Entity Framework y NHibernate.

Supongamos que tiene una entidad Cliente que tiene propiedades para Nombre, Número de teléfono y Pedidos. Nombre y número de teléfono son cadenas regulares, pero Pedidos es una propiedad de navegación que devuelve una lista de todos los pedidos que el cliente haya realizado.

A menudo, es posible que desee consultar a todos sus clientes y obtener su nombre y número de teléfono para llamarlos. Esta es una tarea muy rápida y simple, pero imagínese si cada vez que crea un cliente se realiza automáticamente y se une de manera compleja para devolver miles de pedidos. ¡La peor parte es que ni siquiera va a usar las órdenes, por lo que es un desperdicio completo de recursos!

Este es el lugar perfecto para la carga diferida porque si la propiedad Order es diferida, no obtendrá todos los pedidos del cliente a menos que realmente los necesite. Puede enumerar los objetos del Cliente obteniendo solo su Nombre y Número de teléfono mientras la propiedad Pedido está durmiendo pacientemente, lista para cuando la necesite.

Despertar
fuente
34
Mal ejemplo, ya que la carga diferida generalmente ya está integrada en el ORM. No debe comenzar a agregar valores LaT <T> a sus POCO para obtener una carga diferida, sino que use la forma específica de ORM para hacerlo.
Dynalon
56
@Dyna Este ejemplo se refiere a la carga diferida incorporada de un ORM porque creo que esto ejemplifica la utilidad de la carga diferida de una manera clara y simple.
Despertar
Entonces, si está utilizando Entity Framework, ¿debería uno hacer cumplir su propio vago? ¿O EF lo hace por ti?
Zapnologica
77
@Zapnologica EF hace todo esto por usted por defecto. De hecho, si desea una carga ansiosa (lo contrario de la carga diferida), debe decirle explícitamente a EF mediante el uso Db.Customers.Include("Orders"). Esto hará que la unión de órdenes se ejecute en ese momento en lugar de cuando la Customer.Orderspropiedad se usa por primera vez. La carga diferida también se puede deshabilitar a través de DbContext.
Despertar
2
En realidad, este es un buen ejemplo, ya que uno podría querer agregar esta funcionalidad al usar algo como Dapper.
tbone
41

He estado considerando usar Lazy<T>propiedades para ayudar a mejorar el rendimiento de mi propio código (y para aprender un poco más al respecto). Vine aquí buscando respuestas sobre cuándo usarlo, pero parece que donde quiera que vaya hay frases como:

Utilice la inicialización diferida para diferir la creación de un objeto grande o que requiera muchos recursos, o la ejecución de una tarea que requiera muchos recursos, particularmente cuando tal creación o ejecución podría no ocurrir durante la vida útil del programa.

de la clase MSDN Lazy <T>

Me siento un poco confundido porque no estoy seguro de dónde dibujar la línea. Por ejemplo, considero la interpolación lineal como un cálculo bastante rápido, pero si no necesito hacerlo, ¿puede la inicialización perezosa ayudarme a evitar hacerlo? ¿Vale la pena?

Al final decidí probar mi propia prueba y pensé en compartir los resultados aquí. Desafortunadamente, no soy realmente un experto en hacer este tipo de pruebas y estoy feliz de recibir comentarios que sugieran mejoras.

Descripción

En mi caso, estaba particularmente interesado en ver si Lazy Properties podría ayudar a mejorar una parte de mi código que hace mucha interpolación (la mayor parte no se usa) y, por lo tanto, he creado una prueba que comparó 3 enfoques.

Creé una clase de prueba separada con 20 propiedades de prueba (llamémoslas propiedades t) para cada enfoque.

  • Clase GetInterp: ejecuta interpolación lineal cada vez que se obtiene una propiedad t.
  • Clase InitInterp: Inicializa las propiedades t ejecutando la interpolación lineal para cada una en el constructor. El get solo devuelve un doble.
  • Clase InitLazy: configura las propiedades t como propiedades Lazy para que la interpolación lineal se ejecute una vez cuando se obtiene la propiedad por primera vez. Los resultados posteriores deberían devolver un doble ya calculado.

Los resultados de la prueba se miden en ms y son el promedio de 50 instancias o 20 propiedades obtenidas. Cada prueba se ejecutó 5 veces.

Resultados de la prueba 1: creación de instancias (promedio de 50 instancias)

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

Resultados de la Prueba 2: Primera obtención (promedio de 20 propiedades obtenidas)

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

Resultados de la Prueba 3: Segunda obtención (promedio de 20 propiedades obtenidas)

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

Observaciones

GetInterpes más rápido crear instancias como se esperaba porque no está haciendo nada. InitLazyes más rápido crear instancias que InitInterpsugerir que la sobrecarga en la configuración de propiedades diferidas es más rápida que mi cálculo de interpolación lineal. Sin embargo, estoy un poco confundido aquí porque InitInterpdebería estar haciendo 20 interpolaciones lineales (para configurar sus propiedades t), pero solo toma 0.09 ms para crear una instancia (prueba 1), en comparación con lo GetInterpque toma 0.28 ms para hacer una sola interpolación lineal la primera vez (prueba 2), y 0.1 ms para hacerlo la segunda vez (prueba 3).

Se tarda InitLazycasi 2 veces más que GetInterpobtener una propiedad la primera vez, mientras que InitInterpes la más rápida, ya que poblaba sus propiedades durante la creación de instancias. (Al menos eso es lo que debería haber hecho, pero ¿por qué el resultado de su instanciación fue mucho más rápido que una sola interpolación lineal? ¿Cuándo exactamente está haciendo estas interpolaciones?)

Desafortunadamente, parece que hay una optimización automática de código en mis pruebas. Debería tomar GetInterpel mismo tiempo obtener una propiedad la primera vez que la segunda, pero se muestra más de 2 veces más rápido. Parece que esta optimización también está afectando a las otras clases, ya que todas toman aproximadamente la misma cantidad de tiempo para la prueba 3. Sin embargo, tales optimizaciones también pueden tener lugar en mi propio código de producción, que también puede ser una consideración importante.

Conclusiones

Si bien algunos resultados son los esperados, también hay algunos resultados inesperados muy interesantes, probablemente debido a las optimizaciones de código. Incluso para las clases que parecen estar haciendo mucho trabajo en el constructor, los resultados de creación de instancias muestran que aún pueden ser muy rápidos de crear, en comparación con obtener una propiedad doble. Si bien los expertos en este campo pueden comentar e investigar más a fondo, mi sensación personal es que necesito hacer esta prueba nuevamente, pero en mi código de producción para examinar qué tipo de optimizaciones pueden estar teniendo lugar allí también. Sin embargo, espero que ese InitInterpsea ​​el camino a seguir.

Ben
fuente
26
tal vez deberías publicar tu código de prueba para reproducir tu salida, porque sin conocer tu código será difícil sugerir algo
WiiMaxx
1
Creo que la principal compensación es entre el uso de memoria (diferido) y el uso de CPU (no diferido). Debido a que lazytiene que hacer un poco de contabilidad adicional, InitLazyusaría más memoria que las otras soluciones. También puede tener un impacto de rendimiento menor en cada acceso, mientras verifica si ya tiene un valor o no; Los trucos inteligentes podrían eliminar esa sobrecarga, pero requeriría un soporte especial en IL. (Haskell hace esto haciendo cada valor perezoso una llamada de función, una vez que se genera el valor, y se sustituye por una función que devuelve ese valor cada vez.)
jpaugh
14

Solo para señalar el ejemplo publicado por 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; }
    }
}

antes de que naciera el perezoso lo hubiéramos hecho de esta manera:

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
fuente
66
Siempre uso un contenedor de IoC para esto.
Jowen
1
Estoy totalmente de acuerdo en considerar un contenedor de IoC para esto. Sin embargo, si desea un objeto simple inicializado perezoso simple, considere también que si no necesita que sea seguro para subprocesos, hágalo manualmente con un If puede ser mejor teniendo en cuenta la sobrecarga de rendimiento de cómo se maneja Lazy.
Thulani Chivandikwa
12

De MSDN:

Use una instancia de Lazy para diferir la creación de un objeto grande o que requiera muchos recursos o la ejecución de una tarea que requiera muchos recursos, particularmente cuando tal creación o ejecución podría no ocurrir durante la vida útil del programa.

Además de la respuesta de James Michael Hare, Lazy proporciona una inicialización segura de su valor. Eche un vistazo a la entrada MSDN de enumeración LazyThreadSafetyMode que describe varios tipos de modos de seguridad de subprocesos para esta clase.

Florero
fuente
-2

Debería mirar este ejemplo para comprender la arquitectura de carga diferida

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");
    }
}

-> salida -> 0 1 2

pero si este código no escribe "list.Value.Add (0);"

salida -> Valor no creado

Tufy Duck
fuente