Parece que tenho um hábito estranho ... pelo menos de acordo com meu colega de trabalho. Estamos trabalhando juntos em um pequeno projeto. A maneira como escrevi as aulas é (exemplo simplificado):
[Serializable()]
public class Foo
{
public Foo()
{ }
private Bar _bar;
public Bar Bar
{
get
{
if (_bar == null)
_bar = new Bar();
return _bar;
}
set { _bar = value; }
}
}
Então, basicamente, eu apenas inicializo qualquer campo quando um getter é chamado e o campo ainda é nulo. Imaginei que isso reduziria a sobrecarga ao não inicializar nenhuma propriedade que não fosse usada em nenhum lugar.
ETA: A razão pela qual fiz isso é que minha classe tem várias propriedades que retornam uma instância de outra classe, que por sua vez também possuem propriedades com mais classes e assim por diante. Chamar o construtor para a classe superior subseqüentemente chamaria todos os construtores para todas essas classes, quando elas nem sempre são necessárias.
Há alguma objeção contra essa prática, além da preferência pessoal?
ATUALIZAÇÃO: Eu considerei as muitas opiniões divergentes em relação a esta pergunta e defenderei minha resposta aceita. No entanto, agora cheguei a um entendimento muito melhor do conceito e sou capaz de decidir quando usá-lo e quando não.
Contras:
- Problemas de segurança do thread
- Não obedecendo a uma solicitação "setter" quando o valor passado é nulo
- Micro-otimizações
- O tratamento de exceção deve ocorrer em um construtor
- Precisa verificar o código nulo na classe '
Prós:
- Micro-otimizações
- Propriedades nunca retornam nulas
- Atraso ou evitar o carregamento de objetos "pesados"
A maioria dos contras não é aplicável à minha biblioteca atual, no entanto, eu teria que testar para ver se as "micro-otimizações" estão realmente otimizando alguma coisa.
ÚLTIMA ATUALIZAÇÃO:
Ok, mudei minha resposta. Minha pergunta original era se esse é um bom hábito ou não. E agora estou convencido de que não é. Talvez eu ainda o use em algumas partes do meu código atual, mas não incondicionalmente e definitivamente não o tempo todo. Então, vou perder meu hábito e pensar sobre isso antes de usá-lo. Obrigado a todos!
fonte
Respostas:
O que você tem aqui é uma implementação ingênua de "inicialização lenta".
Resposta curta:
Usar a inicialização lenta incondicionalmente não é uma boa ideia. Ela tem seus lugares, mas é preciso levar em consideração os impactos que esta solução tem.
Antecedentes e explicação:
Implementação concreta:
Vamos primeiro examinar sua amostra concreta e por que considero sua implementação ingênua:
Viola o princípio da menor surpresa (POLS) . Quando um valor é atribuído a uma propriedade, espera-se que esse valor seja retornado. Na sua implementação, este não é o caso de
null
:foo.Bar
threads diferentes podem obter duas instâncias diferentesBar
e um deles ficará sem conexão com aFoo
instância. Quaisquer alterações feitas nessaBar
instância são perdidas silenciosamente.Este é outro caso de violação do POLS. Quando apenas o valor armazenado de uma propriedade é acessado, espera-se que seja seguro para threads. Embora você possa argumentar que a classe simplesmente não é segura para threads - incluindo o getter de sua propriedade -, você teria que documentar isso corretamente, pois esse não é o caso normal. Além disso, a introdução desta questão é desnecessária, como veremos em breve.
Em geral:
agora é hora de examinar a inicialização lenta em geral: a inicialização lenta
geralmente é usada para atrasar a construção de objetos que demoram muito tempo para serem construídos ou que consomem muita memória depois de totalmente construídos.
Essa é uma razão muito válida para usar a inicialização lenta.
No entanto, essas propriedades normalmente não possuem setters, o que elimina o primeiro problema mencionado acima.
Além disso, uma implementação segura para threads seria usada - como
Lazy<T>
- para evitar o segundo problema.Mesmo ao considerar esses dois pontos na implementação de uma propriedade lenta, os seguintes pontos são problemas gerais desse padrão:
A construção do objeto pode não ter êxito, resultando em uma exceção de um getter de propriedade. Esta é mais uma violação do POLS e, portanto, deve ser evitada. Até a seção sobre propriedades nas "Diretrizes de design para desenvolvimento de bibliotecas de classes" afirma explicitamente que os getters de propriedades não devem lançar exceções:
As otimizações automáticas do compilador são prejudicadas, nomeadamente inlining e predição de ramificação. Por favor, veja a resposta de Bill K para uma explicação detalhada.
A conclusão desses pontos é a seguinte:
Para cada propriedade individual implementada preguiçosamente, você deve ter considerado esses pontos.
Isso significa que é uma decisão por caso e não pode ser tomada como uma prática recomendada geral.
Esse padrão tem seu lugar, mas não é uma prática recomendada geral ao implementar classes. Não deve ser usado incondicionalmente , pelas razões expostas acima.
Nesta seção, quero discutir alguns dos pontos que outros apresentaram como argumentos para usar a inicialização lenta incondicionalmente:
Serialização:
EricJ afirma em um comentário:
Existem vários problemas com esse argumento:
Micro-otimização: seu principal argumento é que você deseja construir os objetos somente quando alguém realmente os acessar. Então você está realmente falando sobre como otimizar o uso da memória.
Não concordo com esse argumento pelos seguintes motivos:
Reconheço o fato de que, às vezes, esse tipo de otimização é justificado. Mas, mesmo nesses casos, a inicialização lenta não parece ser a solução correta. Há duas razões para falar contra:
fonte
null
caso ficou muito mais forte. :-)É uma boa escolha de design. Altamente recomendado para código de biblioteca ou classes principais.
É chamado por alguma "inicialização lenta" ou "inicialização atrasada" e geralmente é considerado por todos como uma boa opção de design.
Primeiro, se você inicializar na declaração de variáveis ou construtor no nível de classe, quando seu objeto for construído, você terá a sobrecarga de criar um recurso que nunca poderá ser usado.
Segundo, o recurso é criado apenas se necessário.
Terceiro, você evita a coleta de lixo de um objeto que não foi usado.
Por fim, é mais fácil lidar com exceções de inicialização que podem ocorrer na propriedade e depois com exceções que ocorrem durante a inicialização de variáveis no nível de classe ou do construtor.
Existem exceções à esta regra.
Em relação ao argumento de desempenho da verificação adicional para inicialização na propriedade "get", é insignificante. A inicialização e o descarte de um objeto são uma ocorrência de desempenho mais significativa do que uma simples verificação de ponteiro nulo com um salto.
Diretrizes de design para o desenvolvimento de bibliotecas de classes em http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
A respeito de
Lazy<T>
A
Lazy<T>
classe genérica foi criada exatamente para o que o pôster deseja, consulte Inicialização Lazy em http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx . Se você possui versões mais antigas do .NET, deve usar o padrão de código ilustrado na pergunta. Esse padrão de código se tornou tão comum que a Microsoft considerou adequado incluir uma classe nas bibliotecas .NET mais recentes para facilitar a implementação do padrão. Além disso, se sua implementação precisar de segurança de encadeamento, você deverá adicioná-lo.Tipos de dados primitivos e classes simples
Obvioulsy, você não vai usar a inicialização lenta para o tipo de dados primitivo ou uso de classe simples como
List<string>
.Antes de comentar sobre o Lazy
Lazy<T>
foi introduzido no .NET 4.0, portanto, não adicione mais outro comentário sobre esta classe.Antes de comentar sobre micro-otimizações
Ao criar bibliotecas, você deve considerar todas as otimizações. Por exemplo, nas classes .NET, você verá matrizes de bits usadas para variáveis de classe booleana em todo o código para reduzir o consumo e a fragmentação da memória, apenas para nomear duas "micro-otimizações".
Sobre interfaces de usuário
Você não usará a inicialização lenta para classes usadas diretamente pela interface do usuário. Na semana passada, passei a maior parte do dia removendo o carregamento lento de oito coleções usadas em um modelo de visualização para caixas de combinação. Eu tenho um
LookupManager
que lida com carregamento lento e cache de coleções necessárias por qualquer elemento da interface do usuário."Setters"
Eu nunca usei uma propriedade set ("setters") para qualquer propriedade carregada lenta. Portanto, você nunca permitiria
foo.Bar = null;
. Se você precisar definirBar
, eu criaria um método chamadoSetBar(Bar value)
e não usaria a inicialização lentaColecções
As propriedades da coleção de classes sempre são inicializadas quando declaradas, porque nunca devem ser nulas.
Classes complexas
Deixe-me repetir isso de forma diferente, você usa a inicialização lenta para classes complexas. Que geralmente são classes mal projetadas.
Por último
Eu nunca disse isso para todas as classes ou em todos os casos. É um mau hábito.
fonte
Você considera implementar esse padrão usando
Lazy<T>
?Além da fácil criação de objetos com carregamento lento, você obtém segurança de thread enquanto o objeto é inicializado:
Como outros disseram, você carrega objetos preguiçosamente, se eles são realmente pesados em recursos ou leva algum tempo para carregá-los durante o tempo de construção do objeto.
fonte
Lazy<T>
agora e abster-me de usar da maneira que sempre fazia.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.
Eu acho que depende do que você está inicializando. Provavelmente eu não faria isso por uma lista, já que o custo de construção é muito pequeno, portanto pode ser incluído no construtor. Mas se fosse uma lista pré-preenchida, provavelmente não o faria até que fosse necessária pela primeira vez.
Basicamente, se o custo da construção exceder o custo de fazer uma verificação condicional em cada acesso, crie-o preguiçosamente. Caso contrário, faça-o no construtor.
fonte
A desvantagem que posso ver é que, se você quiser perguntar se Bars é nulo, nunca seria e você criaria a lista lá.
fonte
null
, mas não o recupera. Normalmente, você assume que recebe o mesmo valor que você coloca em uma propriedade.Instanciação / inicialização preguiçosa é um padrão perfeitamente viável. Porém, lembre-se de que, como regra geral, os consumidores da sua API não esperam que os getters e setters gastem um tempo discernível com o POV do usuário final (ou falhem).
fonte
Eu ia apenas comentar a resposta de Daniel, mas sinceramente não acho que isso seja suficiente.
Embora esse seja um padrão muito bom para usar em determinadas situações (por exemplo, quando o objeto é inicializado no banco de dados), é um hábito HORRÍVEL entrar.
Uma das melhores coisas de um objeto é que ele oferece um ambiente seguro e confiável. O melhor caso é se você fizer o maior número possível de campos "Final", preenchendo todos eles com o construtor. Isso torna sua classe bastante à prova de balas. Permitir que os campos sejam alterados por meio de levantadores é um pouco menos, mas não é terrível. Por exemplo:
Com seu padrão, o método toString ficaria assim:
Não apenas isso, mas você precisa de verificações nulas em qualquer lugar que possa usar esse objeto em sua classe (Fora da sua classe é seguro por causa da verificação nula no getter, mas você deve usar principalmente os membros de suas classes dentro da classe)
Além disso, sua classe está perpetuamente em um estado incerto - por exemplo, se você decidiu fazer dessa classe uma classe de hibernação adicionando algumas anotações, como você faria isso?
Se você tomar qualquer decisão com base em alguma micro-optomização sem requisitos e testes, é quase certamente a decisão errada. De fato, há uma chance realmente muito boa de que seu padrão esteja realmente desacelerando o sistema, mesmo nas circunstâncias mais ideais, porque a instrução if pode causar uma falha de previsão de ramificação na CPU, o que atrasará muito as coisas muitas vezes mais do que apenas atribuindo um valor no construtor, a menos que o objeto que você esteja criando seja bastante complexo ou proveniente de uma fonte de dados remota.
Para um exemplo do problema de previsão de brancura (que você está enfrentando repetidamente, quase uma vez), veja a primeira resposta a esta pergunta impressionante: Por que é mais rápido processar uma matriz classificada do que uma matriz não classificada?
fonte
toString()
chamariagetName()
, não usename
diretamente.Permitam-me apenas acrescentar mais um ponto a muitos pontos positivos apresentados por outros ...
O depurador ( por padrão ) avaliará as propriedades ao percorrer o código, o que pode potencialmente instanciar o
Bar
mais cedo do que normalmente aconteceria apenas executando o código. Em outras palavras, o mero ato de depuração está alterando a execução do programa.Isso pode ou não ser um problema (dependendo dos efeitos colaterais), mas é algo para estar ciente.
fonte
Você tem certeza de que Foo deveria instanciar alguma coisa?
Para mim, parece fedorento (embora não necessariamente errado ) deixar Foo instanciar alguma coisa. A menos que seja o objetivo expresso de Foo ser uma fábrica, ela não deve instanciar seus próprios colaboradores, mas injetá-los em seu construtor .
Se, no entanto, o propósito de ser de Foo é criar instâncias do tipo Bar, não vejo nada de errado em fazê-lo preguiçosamente.
fonte