practices - c# variable conventions
¿Buena o mala práctica? Inicializando objetos en getter (9)
¿Considera implementar dicho patrón usando Lazy<T>
?
Además de la creación sencilla de objetos cargados de modo perezoso, obtienes seguridad de hilo mientras el objeto se inicializa:
Como han dicho otros, carga objetos de forma perezosa si realmente pesan demasiado o les lleva algún tiempo cargarlos durante el tiempo de construcción del objeto.
Tengo un hábito extraño, parece ... según mi compañero de trabajo al menos. 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 getter 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 porque mi clase tiene varias propiedades que devuelven una instancia de otra clase, que a su vez también tiene propiedades con 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 necesarias.
¿Hay alguna objeción contra esta práctica, aparte de las preferencias personales?
ACTUALIZACIÓN: He considerado las diferentes opiniones con respecto a esta pregunta y mantendré mi respuesta aceptada. Sin embargo, ahora llegué a una comprensión mucho mejor del concepto y puedo decidir cuándo usarlo y cuándo no.
Contras:
- Temas de seguridad de subprocesos
- No obedecer una solicitud de "incubadora" cuando el valor pasado es nulo
- Micro-optimizaciones
- El manejo de excepciones debe tener lugar en un constructor
- Necesita verificar el nulo en el código de la clase
Pros:
- Micro-optimizaciones
- Las propiedades nunca vuelven nulas
- 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 "micro-optimizaciones" realmente están optimizando algo.
ÚLTIMA ACTUALIZACIÓN:
De acuerdo, cambié mi respuesta. Mi pregunta original era si este es un buen hábito o no. Y ahora estoy convencido de que no es así. Quizás todavía lo use 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 pensar en ello antes de usarlo. ¡Gracias a todos!
¿Estás seguro de que Foo debería estar creando algo instantáneamente?
A mí me parece mal (aunque no necesariamente incorrecto ) dejar que Foo cree una instancia en absoluto. A menos que sea el propósito expreso de Foo ser una fábrica, no debe instanciar sus propios colaboradores, sino que debe ser inyectado en su constructor .
Sin embargo, si el propósito de Foo es crear instancias de tipo Bar, entonces no veo nada de malo en hacerlo de forma perezosa.
Creo que depende de lo que estás inicializando. Probablemente no lo haría por una lista ya que el costo de construcción es bastante pequeño, por lo que puede ir en el constructor. Pero si se trataba de una lista pre-poblada, entonces probablemente no lo haría hasta que la necesitaran 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 perezoso. Si no, hazlo en el constructor.
La desventaja que puedo ver es que si quieres preguntar si Bars es nulo, nunca lo será, y estarías creando la lista allí.
La instanciación / inicialización lenta es un patrón perfectamente viable. Tenga en cuenta, sin embargo, que, como regla general, los consumidores de su API no esperan que los captadores y instaladores tomen un tiempo discernible del POV del usuario final (o que fallen).
Lo que tienes aquí es una ingenuidad: implementación de "inicialización diferida".
Respuesta corta:
Usar la inicialización lenta 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 tu 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 se devuelva este valor. En su implementación, este no es el caso para
null
:foo.Bar = null; Assert.Null(foo.Bar); // This will fail
- Introduce bastante algunos problemas de subprocesamiento: dos llamadas de
foo.Bar
en diferentes subprocesos potencialmente pueden obtener dos instancias diferentes deBar
y una de ellas será sin conexión a la instancia deFoo
. Cualquier cambio realizado en esa instancia deBar
se pierde en silencio.
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, incluida la obtención de su propiedad, tendría que documentarla adecuadamente ya que ese no es el caso normal. Además, la introducción de este tema es innecesaria, como veremos en breve.
En general:
Ahora es el momento de mirar la inicialización lenta en general:
Inicialmente, la inicialización lenta se utiliza para retrasar la construcción de objetos que tardan mucho tiempo en construirse o que requieren mucha memoria una vez que se han construido por completo.
Esa es una razón muy válida para usar la inicialización perezosa.
Sin embargo, tales propiedades normalmente no tienen setters, lo que elimina el primer problema señalado anteriormente.
Además, se usarí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 perezosa, 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 comprador de propiedades. Esta es otra violación más 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 captores de propiedades no deben arrojar excepciones:
Evite arrojar excepciones de los captadores de propiedades.
Los captadores de propiedades deben ser operaciones simples sin condiciones previas. Si un getter arroja una excepción, considere rediseñar la propiedad para que sea un método.
Las optimizaciones automáticas por parte del compilador se ven perjudicadas, a saber, la delimitación y la predicción de bifurcación. Por favor, vea la respuesta de Bill K para una explicación detallada.
La conclusión de estos puntos es la siguiente:
Para cada propiedad individual que se implementa de forma perezosa, 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 debido a los motivos indicados anteriormente.
En esta sección quiero discutir algunos de los puntos que otros han presentado como argumentos para usar incondicionalmente la inicialización diferida:
Publicación por entregas:
EricJ afirma en un comentario:Un objeto que puede ser serializado no tendrá su contructor invocado cuando se deserialice (depende del serializador, pero muchos más comunes se comportan así). Al poner el código de inicialización en el constructor, significa que debe proporcionar soporte adicional para la deserialización. Este patrón evita esa codificación especial.
Hay varios problemas con este argumento:
- La mayoría de los objetos nunca serán serializados. Agregar algún tipo de soporte cuando no es necesario viola YAGNI .
- Cuando una clase necesita admitir la serialización, existen formas de habilitarla sin una solución que no tenga nada que ver con la serialización a primera vista.
Micro-optimización: su argumento principal es que desea construir los objetos solo cuando alguien realmente los acceda. Entonces, en realidad estás hablando de optimizar el uso de la memoria.
No estoy de acuerdo con este argumento por las siguientes razones:- En la mayoría de los casos, algunos objetos más en la memoria no tienen impacto alguno en nada. Las computadoras modernas tienen suficiente memoria. Sin un caso de problemas reales confirmados por un generador de perfiles, esta es una optimización pre-madura y existen buenas razones en contra de ella.
Reconozco el hecho de que a veces este tipo de optimización está justificada. Pero incluso en estos casos, la inicialización lenta no parece ser la solución correcta. Hay dos razones para hablar en contra:
- Inicialización lenta puede dañar el rendimiento. Tal vez solo marginalmente, pero como mostró la respuesta de Bill, el impacto es mayor de lo que uno podría pensar a primera vista. Entonces, este enfoque básicamente intercambia rendimiento versus memoria.
- Si tiene un diseño donde es un caso de uso común usar solo partes de la clase, esto sugiere un problema con el diseño en sí: la clase en cuestión probablemente tenga más de una responsabilidad. La solución sería dividir la clase en varias clases más enfocadas.
Permítanme agregar un punto más a muchos puntos buenos hechos por otros ...
El depurador evaluará ( de forma predeterminada ) las propiedades al recorrer el código, lo que podría crear una instancia de la Bar
antes de lo que normalmente ocurriría si solo se ejecutara el código. En otras palabras, el mero acto de eliminar errores 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.
Solo iba a poner un comentario sobre la respuesta de Daniel, pero sinceramente, no creo que vaya 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 HORRIBLE hábito para entrar.
Una de las mejores cosas de un objeto es que ofrece un entorno seguro y confiable. El mejor caso es si realiza tantos campos como sea posible "Final", llenándolos todos con el constructor. Esto hace que tu clase sea a prueba de balas. Permitir que los campos se cambien a través de setters es un poco menos, pero no terrible. Por ejemplo:
class SafeClass { String name=""; Integer age=0; public void setName(String newName) { assert(newName != null) name=newName; }// follow this pattern for age ... public String toString() { String s="Safe Class has name:"+name+" and age:"+age } }
Con su patrón, el método toString se vería así:
if(name == null) throw new IllegalStateException("SafeClass got into an illegal state! name is null") if(age == null) throw new IllegalStateException("SafeClass got into an illegal state! age is null") public String toString() { String s="Safe Class has name:"+name+" and age:"+age }
No solo esto, sino que necesitas controles nulos donde sea que puedas usar ese objeto en tu clase (Fuera de tu clase está a salvo debido a la verificación nula en el getter, pero deberías usar principalmente los miembros de tu clase dentro de la clase)
Además, su clase está perpetuamente en un estado incierto; por ejemplo, si decidió convertir a esa clase en una clase de hibernación agregando algunas anotaciones, ¿cómo lo haría?
Si toma una decisión basada en una micro-atomización sin requisitos y pruebas, es casi seguro que es una decisión equivocada. De hecho, hay una muy buena posibilidad de que su patrón en realidad esté desacelerando el sistema incluso en las circunstancias más ideales porque la instrucción if puede causar una falla de predicción de bifurcación en la CPU que ralentizará muchas cosas muchas veces más que simplemente asignando un valor en el constructor a menos que el objeto que está creando sea bastante complejo o provenga de un origen de datos remoto.
Para obtener un ejemplo del problema de predicción de brance (en el que se incurre repetidamente, más solo una vez), vea la primera respuesta a esta asombrosa pregunta: ¿Por qué es más rápido procesar una matriz ordenada que una matriz sin clasificar?
Es una buena elección de diseño. Muy recomendable para el código de la biblioteca o las clases principales.
Se llama por alguna "inicialización diferida" o "inicialización retrasada" y, en general, todos la consideran una buena opción de diseño.
En primer lugar, si inicializa en la declaración de variables de nivel de clase o constructor, cuando se construye su objeto, tiene la sobrecarga de crear un recurso que puede que nunca se use.
Segundo, el recurso solo se crea si es necesario.
Tercero, evitas 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 y luego 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 "obtener", es insignificante. Inicializar y eliminar un objeto es un golpe de rendimiento más significativo que un simple control de puntero nulo con un salto.
Pautas de diseño para desarrollar bibliotecas de clase en http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
Con respecto a Lazy<T>
La clase Lazy<T>
genérica se creó exactamente para lo que desea 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, entonces debe agregarla.
Tipos de datos primitivos y clases simples
Obvio, no va a utilizar la inicialización diferida para el tipo de datos primitivo o el uso de clase simple como List<string>
.
Antes de comentar sobre perezoso
Lazy<T>
se introdujo en .NET 4.0, así que no agregue ningún otro comentario con respecto a esta clase.
Antes de comentar sobre micro-optimizaciones
Cuando construya bibliotecas, debe considerar todas las optimizaciones. Por ejemplo, en las clases .NET verá matrices de bits utilizadas para las variables de clase Boolean en todo el código para reducir el consumo de memoria y la fragmentación de la memoria, solo para nombrar dos "micro-optimizaciones".
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 LookupManager
y el almacenamiento en caché de las colecciones que necesita cualquier elemento de interfaz de usuario.
"Setters"
Nunca utilicé una propiedad set ("setters") para ninguna propiedad cargada. Por lo tanto, nunca permitiría foo.Bar = null;
. Si necesita establecer Bar
, crearía un método llamado SetBar(Bar value)
y no usaría la inicialización diferida
Colecciones
Las propiedades de la colección de clases siempre se inicializan cuando se declaran porque nunca deben ser nulas.
Clases complejas
Permíteme repetir esto de manera diferente, usas la inicialización lenta para clases complejas. Que por lo general son clases mal diseñadas.
Finalmente
Nunca dije que hiciera esto para todas las clases o en todos los casos. Es un mal hábito.