Parece que tengo una extraña costumbre ... al menos según mi compañero de trabajo. Hemos estado trabajando juntos en un pequeño proyecto. La forma en que escribí las clases es (ejemplo 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; }
}
}
Entonces, básicamente, solo inicializo cualquier campo cuando se llama a un captador y el campo sigue siendo nulo. Pensé que esto reduciría la sobrecarga al no inicializar ninguna propiedad que no se use en ningún lado.
ETA: La razón por la que hice esto es que mi clase tiene varias propiedades que devuelven una instancia de otra clase, que a su vez también tiene propiedades con aún más clases, y así sucesivamente. Llamar al constructor para la clase superior posteriormente llamaría a todos los constructores para todas estas clases, cuando no siempre son todos necesarios.
¿Hay alguna objeción contra esta práctica, aparte de la preferencia personal?
ACTUALIZACIÓN: He considerado las diferentes opiniones con respecto a esta pregunta y mantendré mi respuesta aceptada. Sin embargo, ahora he llegado a comprender mucho mejor el concepto y puedo decidir cuándo usarlo y cuándo no.
Contras:
- Temas de seguridad de hilos
- No obedecer una solicitud de "establecedor" cuando el valor pasado es nulo
- Micro optimizaciones
- El manejo de excepciones debe tener lugar en un constructor
- Necesito verificar si hay un código nulo en la clase
Pros:
- Micro optimizaciones
- Las propiedades nunca devuelven nulo
- Retrasar o evitar cargar objetos "pesados"
La mayoría de las desventajas no son aplicables a mi biblioteca actual, sin embargo, tendría que probar para ver si las "microoptimizaciones" realmente están optimizando algo.
ÚLTIMA ACTUALIZACIÓN:
Bien, cambié mi respuesta. Mi pregunta original era si este es un buen hábito o no. Y ahora estoy convencido de que no lo es. Tal vez aún lo usaré en algunas partes de mi código actual, pero no incondicionalmente y definitivamente no todo el tiempo. Así que voy a perder mi hábito y pensarlo antes de usarlo. ¡Gracias a todos!
fuente
Respuestas:
Lo que tienes aquí es una implementación ingenua de "inicialización diferida".
Respuesta corta:
Usar la inicialización perezosa incondicionalmente no es una buena idea. Tiene su lugar, pero hay que tener en cuenta los impactos que tiene esta solución.
Antecedentes y explicación:
Implementación concreta:
primero veamos su muestra concreta y por qué considero que su implementación es ingenua:
Viola el Principio de Menos Sorpresa (POLS) . Cuando se asigna un valor a una propiedad, se espera que este valor se devuelva. En su implementación, este no es el caso para
null
:foo.Bar
en subprocesos diferentes pueden obtener dos instancias diferentesBar
y una de ellas no tendrá conexión con laFoo
instancia. Cualquier cambio realizado en esaBar
instancia se pierde silenciosamente.Este es otro caso de violación de POLS. Cuando solo se accede al valor almacenado de una propiedad, se espera que sea seguro para subprocesos. Si bien podría argumentar que la clase simplemente no es segura para subprocesos, incluido el captador de su propiedad, tendría que documentar esto correctamente, ya que ese no es el caso normal. Además, la introducción de este problema es innecesaria, como veremos en breve.
En general:
ahora es el momento de analizar la inicialización diferida en general: la inicialización diferida
se usa generalmente para retrasar la construcción de objetos que tardan mucho tiempo en construirse o que requieren mucha memoria una vez que están completamente construidos.
Esa es una razón muy válida para usar la inicialización diferida.
Sin embargo, tales propiedades normalmente no tienen setters, lo que elimina el primer problema señalado anteriormente.
Además, se utilizaría una implementación segura para subprocesos, como
Lazy<T>
, para evitar el segundo problema.Incluso cuando se consideran estos dos puntos en la implementación de una propiedad diferida, los siguientes puntos son problemas generales de este patrón:
La construcción del objeto podría no tener éxito, lo que da como resultado una excepción de un captador de propiedades. Esta es otra violación de POLS y, por lo tanto, debe evitarse. Incluso la sección sobre propiedades en las "Pautas de diseño para desarrollar bibliotecas de clases" establece explícitamente que los captadores de propiedades no deben lanzar excepciones:
Las optimizaciones automáticas por parte del compilador se ven afectadas, es decir, la alineación y la predicción de ramificaciones. Consulte la respuesta de Bill K para obtener una explicación detallada.
La conclusión de estos puntos es la siguiente:
para cada propiedad individual que se implementa perezosamente, debería haber considerado estos puntos.
Eso significa que es una decisión por caso y no puede tomarse como una mejor práctica general.
Este patrón tiene su lugar, pero no es una mejor práctica general al implementar clases. No debe usarse incondicionalmente , por las razones indicadas anteriormente.
En esta sección, quiero analizar algunos de los puntos que otros han presentado como argumentos para usar la inicialización perezosa incondicionalmente:
Serialización:
EricJ declara en un comentario:
Hay varios problemas con este argumento:
Microoptimización: su argumento principal es que desea construir los objetos solo cuando alguien realmente accede a ellos. Entonces, en realidad estás hablando de optimizar el uso de la memoria.
No estoy de acuerdo con este argumento por las siguientes razones:
Reconozco el hecho de que a veces este tipo de optimización está justificada. Pero incluso en estos casos, la inicialización diferida no parece ser la solución correcta. Hay dos razones para hablar en contra:
fuente
null
caso se hizo mucho más fuerte. :-)Es una buena elección de diseño. Muy recomendable para el código de la biblioteca o las clases principales.
Es llamado por alguna "inicialización diferida" o "inicialización retrasada" y generalmente es considerado por todos como una buena opción de diseño.
Primero, si inicializa en la declaración de variables de nivel de clase o constructor, luego, cuando se construye su objeto, tiene la sobrecarga de crear un recurso que nunca se puede usar.
En segundo lugar, el recurso solo se crea si es necesario.
Tercero, evita que la basura recolecte un objeto que no se usó.
Por último, es más fácil manejar las excepciones de inicialización que pueden ocurrir en la propiedad que las excepciones que ocurren durante la inicialización de las variables de nivel de clase o el constructor.
Hay excepciones a esta regla.
En cuanto al argumento de rendimiento de la verificación adicional para la inicialización en la propiedad "get", es insignificante. La inicialización y disposición de un objeto es un golpe de rendimiento más significativo que una simple comprobación de puntero nulo con un salto.
Pautas de diseño para desarrollar bibliotecas de clases en http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
Respecto a
Lazy<T>
La
Lazy<T>
clase genérica se creó exactamente para lo que quiere el póster, consulte Inicialización diferida en http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx . Si tiene versiones anteriores de .NET, debe usar el patrón de código ilustrado en la pregunta. Este patrón de código se ha vuelto tan común que Microsoft consideró apropiado incluir una clase en las últimas bibliotecas .NET para facilitar la implementación del patrón. Además, si su implementación necesita seguridad de subprocesos, debe agregarla.Tipos de datos primitivos y clases simples
Obviamente, no va a utilizar la inicialización diferida para el tipo de datos primitivos o el uso de clases simples como
List<string>
.Antes de comentar sobre Lazy
Lazy<T>
se introdujo en .NET 4.0, así que por favor no agregue otro comentario sobre esta clase.Antes de comentar sobre micro optimizaciones
Cuando crea bibliotecas, debe considerar todas las optimizaciones. Por ejemplo, en las clases .NET verá matrices de bits utilizadas para las variables de clase booleana en todo el código para reducir el consumo de memoria y la fragmentación de la memoria, solo para nombrar dos "microoptimizaciones".
En cuanto a las interfaces de usuario
No va a utilizar la inicialización diferida para las clases que usa directamente la interfaz de usuario. La semana pasada pasé la mayor parte del día eliminando la carga diferida de ocho colecciones utilizadas en un modelo de vista para cuadros combinados. Tengo un
LookupManager
que maneja la carga diferida y el almacenamiento en caché de las colecciones que necesita cualquier elemento de la interfaz de usuario."Setters"
Nunca he usado una propiedad set ("setters") para ninguna propiedad con carga diferida. Por lo tanto, nunca lo permitirías
foo.Bar = null;
. Si necesita establecer,Bar
entonces crearía un método llamadoSetBar(Bar value)
y no usaría la inicialización diferidaColecciones
Las propiedades de la colección de clases siempre se inicializan cuando se declaran porque nunca deberían ser nulas.
Clases complejas
Déjame repetir esto de manera diferente, usas la inicialización diferida para clases complejas. Que suelen ser clases mal diseñadas.
Por último
Nunca dije que hiciera esto para todas las clases o en todos los casos. Es un mal habito.
fuente
¿Considera implementar tal patrón usando
Lazy<T>
?Además de la creación sencilla de objetos con carga lenta, obtiene seguridad de subprocesos mientras se inicializa el objeto:
Como otros dijeron, usted carga objetos perezosamente si realmente consumen muchos recursos o si lleva algún tiempo cargarlos durante el tiempo de construcción del objeto.
fuente
Lazy<T>
ahora, y me abstendré de usar la forma en que siempre solía hacerlo.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.
Creo que depende de lo que estés inicializando. Probablemente no lo haría para una lista ya que el costo de construcción es bastante pequeño, por lo que puede ir en el constructor. Pero si se tratara de una lista previamente poblada, probablemente no lo haría hasta que fuera necesaria por primera vez.
Básicamente, si el costo de la construcción supera el costo de hacer una verificación condicional en cada acceso, entonces crearlo de forma diferida. Si no, hazlo en el constructor.
fuente
La desventaja que puedo ver es que si quieres preguntar si Bars es nulo, nunca lo sería, y estarías creando la lista allí.
fuente
null
, pero no lo recuperas. Normalmente, asume que recupera el mismo valor que puso en una propiedad.La instanciación / inicialización perezosa es un patrón perfectamente viable. Sin embargo, tenga en cuenta que, como regla general, los consumidores de su API no esperan que los captadores y definidores tomen un tiempo discernible del POV del usuario final (o que fallen).
fuente
Solo iba a poner un comentario sobre la respuesta de Daniel, pero honestamente no creo que llegue lo suficientemente lejos.
Aunque este es un patrón muy bueno para usar en ciertas situaciones (por ejemplo, cuando el objeto se inicializa desde la base de datos), es un hábito HORRIBLE para entrar.
Una de las mejores cosas de un objeto es que ofende un entorno seguro y confiable. El mejor caso es si crea tantos campos como sea posible "Final", llenándolos todos con el constructor. Esto hace que su clase sea bastante a prueba de balas. Permitir que los campos se cambien a través de setters es un poco menos, pero no es terrible. Por ejemplo:
Con su patrón, el método toString se vería así:
No solo esto, sino que necesita verificaciones nulas en todas partes donde posiblemente pueda usar ese objeto en su clase (Fuera de su clase está a salvo debido a la verificación nula en el getter, pero debería usar principalmente los miembros de su clase dentro de la clase)
Además, su clase está perpetuamente en un estado incierto; por ejemplo, si decidiera hacer de esa clase una clase de hibernación agregando algunas anotaciones, ¿cómo lo haría?
Si toma una decisión basada en alguna micro-optomización sin requisitos y pruebas, es casi seguro que es una decisión equivocada. De hecho, existe una muy buena posibilidad de que su patrón realmente esté ralentizando el sistema incluso en las circunstancias más ideales porque la declaración if puede causar un fallo de predicción de ramificación en la CPU, lo que ralentizará las cosas muchas, muchas veces más que solo asigna un valor en el constructor a menos que el objeto que está creando sea bastante complejo o provenga de una fuente de datos remota.
Para ver un ejemplo del problema de predicción de brance (en el que está incurriendo repetidamente, no solo una vez), vea la primera respuesta a esta increíble pregunta: ¿Por qué es más rápido procesar una matriz ordenada que una matriz sin clasificar?
fuente
toString()
llamaríagetName()
, no usarname
directamente.Permítanme agregar un punto más a muchos puntos buenos hechos por otros ...
El depurador evaluará ( por defecto ) las propiedades al recorrer el código, lo que podría crear una instancia
Bar
más pronto de lo que normalmente sucedería simplemente ejecutando el código. En otras palabras, el mero acto de depuración está cambiando la ejecución del programa.Esto puede o no ser un problema (dependiendo de los efectos secundarios), pero es algo a tener en cuenta.
fuente
¿Estás seguro de que Foo debería estar instanciando algo?
A mí me parece maloliente (aunque no necesariamente incorrecto ) dejar que Foo cree una instancia de nada. A menos que el propósito expreso de Foo sea ser una fábrica, no debe crear instancias de sus propios colaboradores, sino inyectarlos en su constructor .
Sin embargo, si el propósito de ser de Foo es crear instancias de tipo Bar, entonces no veo nada de malo en hacerlo perezosamente.
fuente