Quando devo usar o Lazy <T>?

327

Encontrei este artigo sobre Lazy: Preguiça no C # 4.0 - Preguiçosa

Qual é a melhor prática para obter o melhor desempenho usando objetos Lazy? Alguém pode me indicar um uso prático em uma aplicação real? Em outras palavras, quando devo usá-lo?

danyolgiax
fonte
42
Substitui: get { if (foo == null) foo = new Foo(); return foo; }. E existem zilhões de possíveis lugares para usá-lo ...
Kirk Woll
57
Observe que get { if (foo == null) foo = new Foo(); return foo; }não é seguro para threads, enquanto Lazy<T>é seguro para threads por padrão.
Matthew
23
Do MSDN: IMPORTANTE: A inicialização lenta é segura para threads, mas não protege o objeto após a criação. Você deve bloquear o objeto antes de acessá-lo, a menos que o tipo seja seguro para threads.
Pedro.The.Kid

Respostas:

237

Você costuma usá-lo quando deseja instanciar algo na primeira vez que é realmente usado. Isso atrasa o custo de criá-lo até se / quando for necessário, em vez de sempre incorrer no custo.

Geralmente, isso é preferível quando o objeto pode ou não ser usado e o custo de sua construção não é trivial.

James Michael Hare
fonte
121
Por que não usar SEMPRE o Lazy?
TruthOf42
44
Isso incorre no custo no primeiro uso e pode usar alguma sobrecarga de bloqueio (ou sacrificar a segurança da linha, se não) para fazer isso. Portanto, deve ser escolhido com cuidado e não usado, a menos que seja necessário.
James Michael Hare
3
James, você poderia expandir "e o custo de construção não é trivial"? No meu caso, tenho 19 propriedades na minha classe e, na maioria dos casos, apenas 2 ou 3 precisarão ser examinados. Portanto, estou pensando em implementar cada propriedade usando Lazy<T>. No entanto, para criar cada propriedade, estou fazendo uma interpolação linear (ou interpolação bilinear) que é bastante trivial, mas tem algum custo. (Você vai sugerir que eu ir e fazer minha própria experiência?)
Ben
3
James, seguindo meu próprio conselho, fiz meu próprio experimento. Veja meu post .
Ben
17
Você pode querer inicializar / instanciar tudo "durante" a inicialização do sistema para evitar a latência do usuário em sistemas de alto rendimento e baixa latência. Essa é apenas uma das muitas razões para nem sempre usar o Lazy.
Derrick
126

Você deve evitar o uso de Singletons, mas se precisar, Lazy<T>facilita a implementação de singletons preguiçosos e seguros para threads:

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;
}
Mateus
fonte
38
Eu odeio ler Você deve tentar evitar o uso de Singletons quando estou usando-los: D ... agora eu preciso saber por que eu deveria tentar evitá-los: D
Bart Calixto
24
Pararei de usar Singletons quando a Microsoft parar de usá-los em seus exemplos.
precisa saber é o seguinte
4
Costumo discordar da noção de precisar evitar Singletons. Ao seguir o paradigma da injeção de dependência, isso não deve importar de nenhuma maneira. Idealmente, todas as suas dependências devem ser criadas apenas uma vez. Isso reduz a pressão sobre o GC em cenários de alta carga. Portanto, torná-los um Singleton de dentro da própria classe é bom. A maioria (se não todos) dos contêineres DI modernos pode lidar com isso da maneira que você escolher.
Lee Grissom
1
Você não precisa usar um padrão de singleton como esse; em vez disso, use qualquer di-container para configurar sua classe para singleton. O contêiner cuidará da sobrecarga para você.
VivekDev #
Tudo tem um objetivo, há situações em que singletons são uma boa abordagem e situações em que não é :).
Hawkzey 4/03
86

Uma grande no mundo real exemplo de onde o carregamento lento vem a calhar é com do ORM (relação de objeto Mappers), como o Entity Framework e NHibernate.

Digamos que você tenha um cliente da entidade que possua propriedades para Nome, Número de telefone e Pedidos. Name e PhoneNumber são sequências regulares, mas Orders é uma propriedade de navegação que retorna uma lista de todos os pedidos que o cliente já fez.

Em geral, convém consultar todos os clientes e solicitar o nome e o número de telefone deles. Essa é uma tarefa muito rápida e simples, mas imagine que toda vez que você criou um cliente, ele automaticamente fez uma associação complexa para devolver milhares de pedidos. A pior parte é que você nem vai usar os pedidos, portanto é um completo desperdício de recursos!

Este é o local perfeito para carregamento lento, porque se a propriedade Order for preguiçosa, ela não buscará todos os pedidos do cliente, a menos que você realmente precise deles. Você pode enumerar os objetos Customer obtendo apenas seu Nome e Número de Telefone enquanto a propriedade Pedido estiver dormindo pacientemente, pronta para quando você precisar.

Despertar
fonte
34
Exemplo ruim, pois esse carregamento lento geralmente já está incorporado no ORM. Você não deve começar a adicionar valores Lazy <T> aos seus POCOs para obter carregamento lento, mas use a maneira específica do ORM para fazer isso.
Dynalon
56
@Dyna Este exemplo está se referindo ao carregamento lento interno de um ORM porque acho que isso exemplifica a utilidade do carregamento lento de uma maneira clara e simples.
Despertar 23/05
Portanto, se você estiver usando o Entity Framework, deve impor sua própria preguiça? Ou a EF faz isso por você?
Zapnologica 14/01
7
O @Zapnologica EF faz tudo isso por você por padrão. De fato, se você deseja um carregamento rápido (o oposto do carregamento lento), você deve informar explicitamente a EF usando Db.Customers.Include("Orders"). Isso fará com que a junção da ordem seja executada naquele momento, e não quando a Customer.Orderspropriedade é usada pela primeira vez. O Lazy Loading também pode ser desativado através do DbContext.
Despertar
2
Na verdade, este é um bom exemplo, pois é possível adicionar essa funcionalidade ao usar algo como Dapper.
tbone
41

Estou pensando em usar Lazy<T>propriedades para ajudar a melhorar o desempenho do meu próprio código (e para aprender um pouco mais sobre ele). Eu vim aqui procurando respostas sobre quando usá-lo, mas parece que em todos os lugares que vou, há frases como:

Use a inicialização lenta para adiar a criação de um objeto grande ou com muitos recursos, ou a execução de uma tarefa com muitos recursos, principalmente quando essa criação ou execução pode não ocorrer durante a vida útil do programa.

da classe Lazy <T> do MSDN

Fico um pouco confuso porque não sei onde traçar a linha. Por exemplo, considero a interpolação linear como uma computação bastante rápida, mas se não for necessário, a inicialização lenta pode me ajudar a evitar fazê-lo e vale a pena?

No final, decidi tentar meu próprio teste e pensei em compartilhar os resultados aqui. Infelizmente, não sou realmente especialista em fazer esse tipo de teste e, por isso, fico feliz em receber comentários que sugerem melhorias.

Descrição

No meu caso, eu estava particularmente interessado em ver se o Lazy Properties poderia ajudar a melhorar uma parte do meu código que interpolou bastante (a maior parte não sendo usada) e, por isso, criei um teste que comparou três abordagens.

Criei uma classe de teste separada com 20 propriedades de teste (vamos chamá-las de propriedades t) para cada abordagem.

  • Classe GetInterp: executa interpolação linear toda vez que uma propriedade t é obtida.
  • Classe InitInterp: Inicializa as propriedades t executando a interpolação linear para cada uma no construtor. O get apenas retorna um duplo.
  • Classe InitLazy: define as propriedades t como propriedades Lazy, para que a interpolação linear seja executada uma vez quando a propriedade for obtida pela primeira vez. Obtimentos subsequentes devem apenas retornar um dobro já calculado.

Os resultados do teste são medidos em ms e são a média de 50 instanciações ou 20 propriedades obtidas. Cada teste foi então executado 5 vezes.

Resultados do teste 1: Instanciação (média de 50 instanciações)

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 do Teste 2: Primeira obtenção (média de 20 propriedades)

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 do Teste 3: Segunda obtenção (média de 20 propriedades)

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

Observações

GetInterpé mais rápido para instanciar como esperado, porque não está fazendo nada. InitLazyé mais rápido instanciar do que InitInterpsugerir que a sobrecarga na configuração de propriedades preguiçosas é mais rápida que meu cálculo de interpolação linear. No entanto, estou um pouco confuso aqui porque InitInterpdeveria estar fazendo 20 interpolações lineares (para configurar suas propriedades t), mas são necessários apenas 0,09 ms para instanciar (teste 1), em comparação com o GetInterpque leva 0,28 ms para fazer apenas uma interpolação linear na primeira vez (teste 2) e 0,1 ms para fazê-lo na segunda vez (teste 3).

Demora InitLazyquase duas vezes mais do que GetInterppara obter uma propriedade pela primeira vez, enquanto InitInterpé a mais rápida, porque preencheu suas propriedades durante a instanciação. (Pelo menos é o que deveria ter sido feito, mas por que o resultado da instanciação foi muito mais rápido que uma única interpolação linear? Quando exatamente essas interpolações são realizadas?)

Infelizmente, parece que há alguma otimização automática de código em meus testes. Deve levar GetInterpo mesmo tempo para obter uma propriedade na primeira vez que ocorre na segunda vez, mas está sendo exibido duas vezes mais rápido. Parece que essa otimização também está afetando as outras classes, já que elas estão demorando quase o mesmo tempo para o teste 3. No entanto, essas otimizações também podem ocorrer no meu próprio código de produção, o que também pode ser uma consideração importante.

Conclusões

Embora alguns resultados sejam os esperados, também existem resultados inesperados muito interessantes, provavelmente devido a otimizações de código. Mesmo para as classes que parecem estar fazendo muito trabalho no construtor, os resultados da instanciação mostram que eles ainda podem ser muito rápidos de criar, em comparação com a obtenção de uma propriedade dupla. Embora os especialistas nesse campo possam comentar e investigar mais detalhadamente, meu sentimento pessoal é que preciso fazer esse teste novamente, mas no meu código de produção, a fim de examinar que tipo de otimização também pode estar ocorrendo lá. No entanto, espero que esse InitInterpseja o caminho a seguir.

Ben
fonte
26
talvez você deva postar seu código de teste para reproduzir sua saída, porque, sem conhecer seu código, será difícil sugerir alguma coisa.
WiiMaxx 27/11/14
1
Eu acredito que a principal desvantagem é entre uso de memória (lento) e uso de CPU (não lento). Porque lazytem que fazer alguma contabilidade extra, InitLazyusaria mais memória do que as outras soluções. Ele também pode ter um impacto de desempenho menor em cada acesso, enquanto verifica se já possui um valor ou não; truques inteligentes poderiam remover essa sobrecarga, mas exigiria suporte especial na IL. (Haskell faz isso através de todos os valores preguiçoso uma chamada de função, uma vez que o valor é gerado, ele é substituído com uma função que retorna o valor de cada vez.)
jpaugh
14

Apenas para apontar para o exemplo postado 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 do Lazy nascer, teríamos feito assim:

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
fonte
6
Eu sempre uso um contêiner de IoC para isso.
Jowen
1
Concordo plenamente em considerar um contêiner de IoC para isso. Se, no entanto, você quiser um simples singleton de objeto inicial preguiçoso, considere também que, se você não precisar que este seja um thread com segurança, faça-o manualmente com um If, se for melhor, considerar a sobrecarga de desempenho de como o Lazy lida com ele mesmo.
Thulani Chivandikwa
12

Do MSDN:

Use uma instância do Lazy para adiar a criação de um objeto grande ou com muitos recursos ou a execução de uma tarefa com muitos recursos, principalmente quando essa criação ou execução pode não ocorrer durante a vida útil do programa.

Além da resposta de James Michael Hare, o Lazy fornece uma inicialização segura do seu valor. Dê uma olhada na entrada MSDN da enumeração LazyThreadSafetyMode que descreve vários tipos de modos de segurança de encadeamento para esta classe.

Vasea
fonte
-2

Você deve procurar neste exemplo para entender a arquitetura Lazy Loading

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

-> saída -> 0 1 2

mas se esse código não escrever "list.Value.Add (0);"

saída -> Valor não criado

Tufy Duck
fonte