c++ - relaciones - ¿Cuánto trabajo se debe hacer en un constructor?
relaciones entre clases c++ (18)
En caso de que las operaciones que podrían llevar algún tiempo se realicen en un constructor o se construya el objeto, se inicializarán más tarde.
Por ejemplo, cuando se construye un objeto que representa una estructura de directorio, la población del objeto y sus elementos secundarios deben hacerse en el constructor. Claramente, un directorio puede contener directorios y que a su vez puede contener directorios, etc.
¿Cuál es la solución elegante para esto?
Asegúrese de que el controlador no haga nada que pueda arrojar una excepción.
Depende (respuesta típica de CS). Si está construyendo objetos al inicio para un programa de larga ejecución, entonces no hay problema con hacer mucho trabajo en constructores. Si esto es parte de una GUI donde se espera una respuesta rápida, podría no ser apropiado. Como siempre, la mejor respuesta es intentarlo de la manera más simple primero, perfilar y optimizar desde allí.
Para este caso específico, puede hacer una construcción diferida de los objetos del subdirectorio. Solo crea entradas para los nombres de los directorios de nivel superior. Si se accede, luego cargue el contenido de ese directorio. Repita esto cuando el usuario expulse la estructura del directorio.
El tiempo requerido no debe ser una razón para no poner algo en un constructor. Puede poner el código en una función privada y llamarlo desde su constructor, solo para mantener el código en el constructor despejado.
Sin embargo, si lo que desea hacer no es necesario para darle al objeto una condición definida, y podría hacer eso más adelante en el primer uso, este sería un argumento razonable para apagarlo y hacerlo más tarde. Pero no lo haga dependiente de los usuarios de su clase: estas cosas (inicialización bajo demanda) deben ser completamente transparentes para los usuarios de su clase. De lo contrario, invariantes importantes de su objeto podrían romperse fácilmente.
En cuanto a la cantidad de trabajo que se debe hacer en el constructor, yo diría que debería tener en cuenta qué tan lentos son las cosas, cómo vas a usar la clase y, en general, cómo te sientes al respecto.
En su objeto de estructura de directorios: recientemente implementé un navegador samba (windows shares) para mi HTPC, y como era increíblemente lento, opté por inicializar un directorio cuando se tocó. Por ejemplo, primero, el árbol consistiría en solo una lista de máquinas, luego, cada vez que busque en un directorio, el sistema inicializaría automáticamente el árbol de esa máquina y obtendría un directorio con un nivel más profundo, y así sucesivamente.
Idealmente, creo que podría incluso llegar tan lejos como escribir un hilo de trabajo que escanee los directorios en primer lugar, y le daría prioridad al directorio que está explorando actualmente, pero en general eso es demasiado trabajo para algo simple;)
Estoy de acuerdo en que los constructores de larga ejecución no son intrínsecamente malos. Pero yo diría que tu eres casi siempre lo que no debes hacer. Mi consejo es similar al de Hugo, Rich y Litb:
- mantenga el trabajo que hace en los constructores al mínimo: mantenga el enfoque en el estado de inicialización.
- No arroje desde constructores a menos que no pueda evitarlo. Intento solo lanzar std :: bad_alloc.
- No llame al sistema operativo ni a las API de la biblioteca a menos que sepa lo que hacen, la mayoría puede bloquear. Se ejecutarán rápidamente en su caja de desarrollo y en las máquinas de prueba, pero en el campo pueden bloquearse durante largos periodos de tiempo ya que el sistema está ocupado haciendo otra cosa.
- Nunca, nunca haga I / O en un constructor - de ningún tipo. La E / S está comúnmente sujeta a todo tipo de latencias muy largas (de milisegundos a segundos). I / O incluye
- E / S de disco
- Cualquier cosa que use la red (incluso indirectamente) Recuerde que la mayoría de los recursos pueden estar fuera de la caja.
Ejemplo de problema de E / S: muchos discos duros tienen un problema cuando entran en un estado en el que no se actualizan las lecturas o escrituras durante 100 o incluso miles de milisegundos. La primera y la generación de unidades de estado sólido hacen esto a menudo. El usuario ahora tiene la posibilidad de saber que el jus de su programa colgó un poco; simplemente piensan que es su software defectuoso.
Por supuesto, la maldad de un constructor de larga ejecución depende de dos cosas:
- Qué significa ''largo''
- La frecuencia con que en un período determinado se construyen objetos con los constructores ''largos''.
Ahora, si ''largo'' es simplemente unos 100 ciclos de trabajo extra, entonces no es realmente largo. Pero un constructor está entrando en el rango de 100 microsegundos, sugiero que es bastante largo. Por supuesto, si solo estás instanciando uno de estos, o creando instancias de ellos raramente (digamos uno cada pocos segundos), entonces no es probable que veas problemas debido a una duración en este rango.
La frecuencia es un factor importante, un administrador de 500 no es un problema si solo está construyendo algunos de ellos, pero crear un millón de ellos representaría un problema de rendimiento significativo.
Vamos a hablar sobre su ejemplo: poblar un árbol de objetos de directorio dentro del objeto "Directorio de clases". (nota, voy a suponer que este es un programa con una interfaz gráfica de usuario). Aquí, la duración de su CTOR no depende del código que escriba, sino del tiempo que lleva enumerar un árbol de directorios arbitrariamente grande. Esto es suficientemente malo en el disco duro local. Es aún más problemático en resurce remoto (en red).
Ahora, imagine hacerlo en su subproceso de interfaz de usuario: su UI se detendrá en seco durante segundos, 10 segundos o incluso minutos. En Windows llamamos a esto un bloqueo de IU. Son malos, malos, malos (sí, los tenemos ... sí, trabajamos duro para eliminarlos).
Los bloqueos de UI son algo que puede hacer que la gente realmente odie tu software.
Lo correcto aquí es simplemente inicializar los objetos de directorio. Cree su árbol de directorios en un bucle que pueda cancelarse y mantenga su UI en estado de respuesta (el botón cancelar siempre debería funcionar)
Excelente pregunta: el ejemplo que dio cuando un objeto ''Directorio'' tiene referencias a otros objetos ''Directorio'' también es un gran ejemplo.
Para este caso específico movería el código para construir objetos subordinados fuera del constructor (o tal vez haga el primer nivel [hijos inmediatos] como otro post aquí recomienda), y tengo un mecanismo separado de "inicialización" o "compilación").
Hay otro problema potencial que, de lo contrario, más allá del simple rendimiento, es la huella de memoria: si termina haciendo llamadas recursivas muy profundas, también terminará con problemas de memoria [ya que la pila guardará copias de todas las variables locales hasta que termine la recursión].
Históricamente, he codificado mis constructores para que el objeto esté listo para usar una vez que el método constructor esté completo. La cantidad o la cantidad de código involucrado depende de los requisitos para el objeto.
Por ejemplo, supongamos que necesito mostrar la siguiente clase de empresa en una vista de detalles:
public class Company
{
public int Company_ID { get; set; }
public string CompanyName { get; set; }
public Address MailingAddress { get; set; }
public Phones CompanyPhones { get; set; }
public Contact ContactPerson { get; set; }
}
Como quiero mostrar toda la información que tengo sobre la empresa en la vista de detalles, mi constructor contendrá todo el código necesario para completar cada propiedad. Dado que es un tipo complejo, el constructor de la Compañía también activará la ejecución del constructor Dirección, Teléfonos y Contacto.
Ahora, si estoy rellenando una vista de listado de directorios, donde es posible que solo necesite CompanyName y el número de teléfono principal, puedo tener un segundo constructor en la clase que solo recupere esa información y deja la información restante vacía, o simplemente puedo crear un objeto separado que solo contiene esa información. Realmente solo depende de cómo se recupera la información y de dónde.
Independientemente del número de constructores en una clase, mi objetivo personal es hacer cualquier procesamiento que sea necesario para preparar el objeto para cualquier tarea que se le pueda imponer.
Intenta tener lo que crees que es necesario y no pienses si será lento o rápido. La optimización previa es una pérdida de tiempo, así que codifíquela, perfile y optimícela si es necesario.
Las matrices de objetos siempre usarán el constructor predeterminado (sin argumentos). No hay forma de evitar eso.
Hay constructores "especiales": el constructor de copias y el operador = ().
¡Puedes tener muchos constructores! O termina con muchos constructores más adelante. De vez en cuando, Bill out in la-la land quiere un nuevo constructor con flotadores en lugar de dobles para salvar esos 4 bytes pésimos. (¡Compre algo de RAM Bill!)
No se puede llamar al constructor como se puede hacer con un método ordinario para volver a invocar esa lógica de inicialización.
No puede hacer que la lógica del constructor sea virtual y cambiarla en una subclase. (Aunque si está invocando un método initialize () desde el constructor en lugar de hacerlo manualmente, los métodos virtuales no funcionarán).
.
Todas estas cosas crean mucha pena cuando existe una lógica significativa en el constructor. (O al menos duplicación de código).
Entonces, como opción de diseño, prefiero tener constructores mínimos que (opcionalmente, dependiendo de sus parámetros y la situación) invoquen un método initialize ().
Dependiendo de las circunstancias, initialize () puede ser privado. O puede ser público y admitir invocaciones múltiples (por ejemplo, reinicialización).
.
En última instancia, la elección aquí varía según la situación. Tenemos que ser flexibles y considerar las compensaciones. No hay una talla para todos.
El enfoque que usaríamos para implementar una clase con una única instancia solitaria que esté utilizando subprocesos para hablar con una pieza de hardware dedicada y que tenga que escribirse en 1/2 hora no es necesariamente lo que usaríamos para implementar de una clase representando las matemáticas en números de coma flotante de precisión variable escritos durante muchos meses.
Los trabajos más importantes de un constructor es dar al objeto un estado válido inicial. La expectativa más importante para el constructor, en mi opinión, es que el constructor no debería tener EFECTOS SECUNDARIOS.
Para resumir:
Como mínimo, su constructor necesita configurar el objeto hasta el punto en que sus invariantes sean verdaderas.
Su elección de invariantes puede afectar a sus clientes. (¿El objeto promete estar listo para el acceso en todo momento? ¿O solo en ciertos estados?) Un constructor que se encarga de todos los preparativos puede simplificar la vida para los clientes de la clase.
Los constructores de larga ejecución no son inherentemente malos, pero pueden ser malos en algunos contextos.
Para los sistemas que involucran una interacción del usuario, los métodos de larga duración de cualquier tipo pueden llevar a una capacidad de respuesta deficiente y deben evitarse.
Retrasando el cálculo hasta después de que el constructor pueda ser una optimización efectiva; puede resultar innecesario realizar todo el trabajo. Esto depende de la aplicación, y no debe determinarse prematuramente.
En general, depende.
Por el mantenimiento del código, las pruebas y la depuración, intento evitar poner cualquier lógica en los constructores. Si prefiere que la lógica se ejecute desde un constructor, entonces es útil poner la lógica en un método como init () y llamar a init () desde el constructor. Si planea desarrollar pruebas unitarias, debe evitar poner cualquier lógica en un constructor, ya que puede ser difícil probar diferentes casos. Creo que los comentarios anteriores ya abordan esto, pero ... si su aplicación es interactiva, debería evitar tener una sola llamada que conduzca a un golpe de rendimiento notable. Si su aplicación no es interactiva (por ejemplo, trabajo por lotes nocturno), un solo golpe de rendimiento no es tan importante.
Por lo general, no desea que el constructor realice ningún cálculo. Alguien más que use el código no esperará que haga más que una configuración básica.
Para un árbol de directorios como el que está hablando, la solución "elegante" probablemente no sea construir un árbol completo cuando se construye el objeto. En cambio, compilarlo según demanda. Puede que a alguien que use su objeto no le importe realmente lo que hay en los subdirectorios, así que empiece simplemente haciendo que su lista de constructores sea el primer nivel, y luego, si alguien desea descender a un directorio específico, compile esa parte del árbol cuando lo soliciten. eso.
RAII es la columna vertebral de la administración de recursos de C ++, así que adquiera los recursos que necesita en el constructor, libérelos en el destructor.
Esto es cuando establece invariantes su clase. Si lleva tiempo, lleva tiempo. Cuantos menos constructos "si X existe, haga Y" que tenga, más simple será el diseño del resto de la clase. Más tarde, si los perfiles muestran que esto es un problema, considere optimizaciones como la inicialización diferida (adquisición de recursos cuando los necesita por primera vez).
Realmente depende del contexto, es decir, del problema que la clase debe resolver. ¿Debería, por ejemplo, ser capaz de mostrar los niños actuales dentro de sí mismo? Si la respuesta es sí, entonces los niños no deberían cargarse en el constructor. Por otro lado, si la clase representa una instantánea de una estructura de directorio, entonces puede cargarse en el constructor.
Si se puede hacer algo fuera de un constructor, evite hacerlo adentro. Más tarde, cuando sepas que tu clase se comporta bien, podrías arriesgarte a hacerlo por dentro.
Tanto como sea necesario y nada más.
El constructor debe poner el objeto en un estado utilizable, por lo tanto, como mínimo, las variables de clase deben ser iniciadas. Lo que inició significa puede tener una interpretación amplia. Aquí hay un ejemplo artificial. ¡Imagine que tiene una clase que tiene la responsabilidad de proporcionar N! a su aplicación de llamada.
Una forma de implementarlo sería hacer que el constructor no haga nada, con una función miembro con un bucle que calcula el valor necesario y lo devuelve.
Otra forma de implementarlo sería tener una variable de clase que sea una matriz. El constructor establecería todos los valores en -1, para indicar que el valor aún no se ha calculado. la función miembro haría una evaluación perezosa. Mira el elemento de la matriz. Si es -1, lo calcula, lo almacena y devuelve el valor; de lo contrario, solo devuelve el valor de la matriz.
Otra forma de implementarlo sería como la última, solo que el constructor precalculará los valores y poblará la matriz, por lo que el método podría extraer el valor de la matriz y devolverlo.
Otra forma de implementarlo sería mantener los valores en un archivo de texto, y usar N como base para una compensación en el archivo para extraer el valor. En este caso, el constructor abriría el archivo, y el destructor cerraría el archivo, mientras que el método haría algún tipo de fseek / fread y devolvería el valor.
Otra forma de implementarlo sería precomputar los valores y almacenarlos como una matriz estática a la que la clase puede hacer referencia. El constructor no tendría trabajo, y el método llegaría a la matriz para obtener el valor y devolverlo. Múltiples instancias compartirían esa matriz.
Dicho todo esto, lo que debe centrarse es que, en general, desea poder llamar al constructor una vez y luego usar los otros métodos con frecuencia. Si hacer más trabajo en el constructor significa que sus métodos tienen menos trabajo que hacer y ejecutar más rápido, entonces es una buena compensación. Si está construyendo / destruyendo mucho, como en un bucle, entonces probablemente no sea una buena idea tener un alto costo para su constructor.
Voto por constructores delgados, y agregue un comportamiento de estado extra "no inicializado" a su objeto en ese caso.
La razón: si no lo hace, impone a todos sus usuarios que también tengan constructores pesados o que asignen su clase de forma dinámica. En ambos casos, puede verse como una molestia.
Puede ser difícil detectar errores de dichos objetos si se vuelven estáticos, ya que el constructor se ejecuta antes de main () y es más difícil de rastrear para un depurador.