Quanto lavoro dovrebbe essere svolto in un costruttore?

Le operazioni che potrebbero richiedere del tempo devono essere eseguite in un costruttore o l’object deve essere costruito e quindi inizializzato più tardi.

Ad esempio, quando si costruisce un object che rappresenta una struttura di directory, la popolazione dell’object e i relativi figli devono essere eseguiti nel costruttore. Chiaramente, una directory può contenere directory e, a sua volta, può contenere directory e così via.

Qual è la soluzione elegante a questo?

Storicamente, ho codificato i miei costruttori in modo che l’object sia pronto per l’uso una volta completato il metodo del costruttore. Quanto o quanto poco codice è coinvolto dipende dai requisiti per l’object.

Ad esempio, supponiamo di dover visualizzare la seguente class aziendale in una visualizzazione dettagli:

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

Poiché desidero visualizzare tutte le informazioni che ho sull’azienda nella vista dettagli, il mio costruttore conterrà tutto il codice necessario per popolare ogni proprietà. Dato che è un tipo complesso, il costruttore dell’Azienda attiverà anche l’esecuzione del costruttore Address, Phones e Contact.

Ora, se sto compilando una vista elenco directory, dove potrei aver bisogno solo del CompanyName e del numero di telefono principale, potrei avere un secondo costruttore sulla class che recupera solo tali informazioni e lascia le informazioni rimanenti vuote, o posso solo creare un object separato che contiene solo quell’informazione. Dipende davvero solo da come vengono recuperate le informazioni e da dove.

Indipendentemente dal numero di costruttori di una class, il mio objective personale è quello di fare qualsiasi elaborazione è necessaria per preparare l’object per qualsiasi compito possa essere imposto su di esso.

Riassumere:

  • Come minimo, il tuo costruttore ha bisogno di ottenere l’object configurato al punto che i suoi invarianti siano veri.

  • La tua scelta di invarianti può influenzare i tuoi clienti. (L’object promette di essere pronto per l’accesso in qualsiasi momento? O solo solo in alcuni stati?) Un costruttore che si occupa di tutto il set up up-front può rendere la vita più semplice per i clienti della class.

  • I costruttori con esecuzione prolungata non sono intrinsecamente cattivi, ma potrebbero non funzionare in alcuni contesti.

  • Per i sistemi che implicano un’interazione dell’utente, i metodi a lungo termine di qualsiasi tipo possono portare a una scarsa reattività e dovrebbero essere evitati.

  • Ritardare il calcolo fino a dopo che il costruttore potrebbe essere un’ottimizzazione efficace; potrebbe rivelarsi non necessario eseguire tutto il lavoro. Questo dipende dall’applicazione e non dovrebbe essere determinato prematuramente.

  • Nel complesso, dipende.

Di solito non si vuole che il costruttore esegua alcun calcolo. Qualcun altro che usa il codice non si aspetterà che faccia più di una semplice configurazione di base.

Per un albero di directory come quello di cui parli, la soluzione “elegante” probabilmente non costruisce un albero completo quando l’object è costruito. Invece, costruiscilo su richiesta. Qualcuno che usa il tuo object potrebbe non interessarsi di cosa si trova nelle sottodirectory, quindi inizia facendo in modo che il tuo costruttore elenchi il primo livello, e poi se qualcuno vuole discendere in una directory specifica, allora costruisci quella porzione dell’albero quando lo richiedono esso.

Il tempo richiesto non dovrebbe essere una ragione per non mettere qualcosa in un costruttore. Puoi inserire il codice stesso in una funzione privata e chiamarlo fuori dal tuo costruttore, solo per mantenere il codice nel costruttore chiaro.

Tuttavia, se le cose che vuoi fare non sono obbligate a dare all’object una condizione definita, e potresti farlo più tardi al primo utilizzo, questo sarebbe un argomento ragionevole per metterlo fuori e farlo in un secondo momento. Ma non renderlo dipendente dagli utenti della tua class: Queste cose (inizializzazione su richiesta) devono essere completamente trasparenti per gli utenti della tua class. Altrimenti, invarianti importanti del tuo object potrebbero facilmente rompersi.

Dipende (tipica risposta CS). Se stai costruendo oggetti all’avvio per un programma a esecuzione prolungata, non c’è alcun problema nel fare molto lavoro nei costruttori. Se questo fa parte di una GUI in cui è prevista una risposta rapida, potrebbe non essere appropriato. Come sempre, la migliore risposta è provarlo il modo più semplice per prima, il profilo e ottimizzare da lì.

In questo caso specifico, è ansible eseguire la costruzione lenta degli oggetti della sottodirectory. Crea solo voci per i nomi delle directory di livello superiore. Se sono accessibili, quindi caricare il contenuto di tale directory. Ripeti l’operazione mentre l’utente espelle la struttura della directory.

I lavori più importanti di un costruttore sono di dare all’object uno stato valido iniziale. L’aspettativa più importante per il costruttore, secondo me, è che il costruttore non debba avere EFFETTI COLLATERALI.

Per motivi di manutenzione, test e debug del codice, cerco di evitare di inserire qualsiasi logica nei costruttori. Se si preferisce eseguire la logica da un costruttore, è utile inserire la logica in un metodo come init () e chiamare init () dal costruttore. Se si pianifica lo sviluppo di test unitari, evitare di inserire qualsiasi logica in un costruttore poiché potrebbe essere difficile testare casi diversi. Penso che i commenti precedenti lo facciano già, ma … se la tua applicazione è intertriggers, dovresti evitare di avere una singola chiamata che porti a un notevole calo delle prestazioni. Se la tua applicazione non è intertriggers (es: lavoro batch notturno), un singolo colpo di performance non è un grosso problema.

Concordo sul fatto che i costruttori di lunga durata non siano intrinsecamente cattivi. Ma direi che la tua è quasi sempre la cosa sbagliata da fare. Il mio consiglio è simile a quello di Hugo, Rich e Litb:

  1. mantieni il lavoro che fai nei costruttori al minimo – mantieni l’attenzione sullo stato di inizializzazione.
  2. Non buttare dai costruttori a meno che tu non possa evitarlo. Provo a lanciare solo std :: bad_alloc.
  3. Non chiamare le API del sistema operativo o della libreria a meno che tu non sappia cosa fanno – la maggior parte può bloccare. Verranno eseguiti rapidamente sulla tua casella di sviluppo e sulle macchine di prova, ma sul campo possono essere bloccati per lunghi periodi di tempo poiché il sistema è impegnato a fare qualcos’altro.
  4. Mai, mai fare I / O in un costruttore – di qualsiasi tipo. I / O è comunemente sobject a tutti i tipi di latenze molto lunghe (100 di millisecondi a secondi). I / O include
    • Disk I / O
    • Tutto ciò che utilizza la rete (anche indirettamente) Ricorda la maggior parte delle risorse può essere off-box.

Esempio di problemi di I / O: molti dischi rigidi hanno un problema in cui entrano in uno stato in cui non effettuano letture o scritture di servizio per 100 o anche migliaia di millisecondi. Le unità a stato solido di prima e di generazione lo fanno spesso. L’utente ha ora modo di sapere che il tuo programma è semplicemente sospeso per un po ‘- pensano solo che sia il tuo software bacato.

Naturalmente, la cattiveria di un costruttore a lunga esecuzione dipende da due cose:

  1. Cosa significa “lungo”
  2. La frequenza con cui in un determinato periodo vengono costruiti oggetti con costruttori “lunghi”.

Ora, se ‘long’ è semplicemente un centinaio di cicli di lavoro in più, allora non è molto lungo. Ma un costruttore sta entrando nei 100 della gamma di microsecondi che suggerisco che sia piuttosto lungo. Naturalmente, se si sta solo creando un’istanza di uno di questi, o istanziandoli raramente (diciamo uno ogni pochi secondi), allora non è probabile che si vedano problemi a causa di una durata in questo intervallo.

La frequenza è un fattore importante, un 500 ustor non è un problema se ne costruisci solo alcuni: ma la creazione di un milione di questi porrà un significativo problema di prestazioni.

Parliamo del tuo esempio: popolamento di un albero di oggetti di directory all’interno dell’object “class Directory”. (nota, assumerò che questo è un programma con un’interfaccia grafica). Qui, la durata del CTOR non dipende dal codice che scrivi – il suo convenuto sul tempo necessario per enumerare un albero di directory arbitrariamente grande. Questo è abbastanza brutto sul disco rigido locale. È ancora più problematico per il resurce remoto (in rete).

Ora, immagina di fare questo sul tuo thread dell’interfaccia utente: l’interfaccia utente si bloccherà nelle sue tracce per secondi, 10 secondi di secondi o potenziali minuti pari. In Windows chiamiamo questo un blocco dell’interfaccia utente. Sono cattivi cattivi (sì, li abbiamo … sì, lavoriamo sodo per eliminarli).

Gli hang UI sono qualcosa che può far odiare davvero il tuo software.

La cosa giusta da fare qui è semplicemente inizializzare gli oggetti della directory. Costruisci l’albero delle directory in un ciclo che può essere annullato e mantiene l’interfaccia utente in uno stato di risposta (il pulsante Annulla dovrebbe sempre funzionare)

Per quanto riguarda il lavoro da fare nel costruttore, direi che dovrebbe tener conto di quanto siano lente le cose, di come userete la class e in generale di come vi sentite personalmente.

Sull’object della struttura della directory: ho recentemente implementato un browser samba (windows shares) per il mio HTPC e, poiché ciò è stato incredibilmente lento, ho deciso di inizializzare una directory solo quando è stata toccata. Ad esempio, in primo luogo l’albero sarebbe costituito da un elenco di macchine, quindi ogni volta che si accede a una directory, il sistema inizializzerà automaticamente l’albero da quella macchina e otterrà la directory che elenca un livello più in profondità, e così via.

Idealmente, penso che potresti anche prenderlo fino alla scrittura di un thread di lavoro che analizza le directory in ampiezza e darebbe priorità alla directory che stai attualmente navigando, ma generalmente è troppo lavoro per qualcosa di semplice;)

Assicurati che il ctor non faccia nulla che possa generare un’eccezione.

Quanto necessario e non di più.

Il costruttore deve mettere l’object in uno stato utilizzabile, quindi almeno le variabili di class dovrebbero essere inattive. Quali mezzi resi vuoti possono avere un’interpretazione ampia. Ecco un esempio forzato. Immagina di avere una class che ha la responsabilità di fornire N! alla tua applicazione di chiamata.

Un modo per implementarlo sarebbe avere il costruttore che non fa nulla, con una funzione membro con un ciclo che calcola il valore necessario e restituisce.

Un altro modo per implementarlo sarebbe avere una variabile di class che sia una matrice. Il costruttore imposta tutti i valori su -1, per indicare che il valore non è stato ancora calcolato. la funzione membro farebbe una valutazione pigra. Guarda l’elemento dell’array. Se è -1, lo calcola e lo memorizza e restituisce il valore, altrimenti restituisce semplicemente il valore dall’array.

Un altro modo per implementarlo sarebbe proprio come l’ultimo, solo il costruttore calcolerebbe i valori e compilerebbe l’array, quindi il metodo potrebbe semplicemente estrarre il valore dall’array e restituirlo.

Un altro modo per implementarlo sarebbe mantenere i valori in un file di testo e utilizzare N come base per un offset nel file da cui estrarre il valore. In questo caso, il costruttore aprirà il file e il distruttore chiuderà il file, mentre il metodo eseguirà una sorta di fseek / fread e restituirà il valore.

Un altro modo per implementarlo è precomputare i valori e archiviarli come array statici a cui la class può fare riferimento. Il costruttore non avrebbe funzionato e il metodo avrebbe raggiunto l’array per ottenere il valore e restituirlo. Più istanze avrebbero condiviso quell’array.

Tutto ciò che viene detto, la cosa su cui concentrarsi, è che generalmente si vuole essere in grado di chiamare il costruttore una volta, quindi usare frequentemente gli altri metodi. Se fare più lavoro nel costruttore significa che i tuoi metodi hanno meno lavoro da fare e correre più velocemente, allora è un buon compromesso. Se stai costruendo / distruggendo molto, come in un ciclo, allora probabilmente non è una buona idea avere un costo elevato per il tuo costruttore.

Se qualcosa può essere fatto al di fuori di un costruttore, evita di farlo all’interno. Più tardi, quando sai che la tua class è altrimenti ben educata, potresti rischiare di farla dentro.

RAII è la spina dorsale della gestione delle risorse in C ++, quindi acquisisci le risorse necessarie nel costruttore, rilasciandole nel distruttore.

Questo è quando stabilisci le invarianti di class. Se ci vuole tempo, ci vuole tempo. Il minor numero di “se X esiste do Y” è costruito, più semplice sarà il resto della class da progettare. Successivamente, se la profilazione mostra che questo è un problema, considera le ottimizzazioni come l’inizializzazione pigra (l’acquisizione di risorse quando ne hai bisogno per la prima volta).

Dipende davvero dal contesto, cioè dal problema che la class deve risolvere. Dovrebbe, ad esempio, essere sempre in grado di mostrare gli attuali bambini dentro di sé? Se la risposta è sì, i bambini non dovrebbero essere caricati nel costruttore. D’altra parte, se la class rappresenta un’istantanea di una struttura di directory, può essere caricata nel costruttore.

Io voto per i costruttori sottili e aggiungo un comportamento di stato “non inizializzato” al tuo object in quel caso.

La ragione: se non lo fai, imponi a tutti i tuoi utenti di avere anche costruttori pesanti o, per allocare la tua class dynamicmente. In entrambi i casi può essere visto come una seccatura.

Può essere difficile catturare errori da tali oggetti se diventano statici, perché il costruttore viene eseguito prima di main () ed è più difficile da tracciare per il debugger.

Ottima domanda: l’esempio che hai dato dove un object ‘Directory’ ha riferimenti ad altri oggetti ‘Directory’ è anche un ottimo esempio.

In questo caso specifico sposterei il codice per creare oggetti subordinati dal costruttore (o forse fare il primo livello [bambini immediati] come un altro post qui raccomanda), e avere un meccanismo separato ‘inizializza’ o ‘build’).

C’è un altro potenziale problema, al di là delle sole prestazioni, che è l’impronta della memoria: se finisci per fare chiamate ricorsive molto profonde, probabilmente avrai anche problemi di memoria [dato che lo stack conserverà le copie di tutte le variabili locali fino a quando la ricorsione non termina].

Cerca di avere quello che pensi sia necessario lì e non pensare se sarà lento o veloce. La pre-ottimizzazione è una perdita di tempo, quindi codificalo, definiscilo e ottimizzalo se necessario.

Le matrici di oggetti usano sempre il costruttore predefinito (no-arguments). Non c’è modo di aggirare questo.

Esistono costruttori “speciali”: il costruttore e l’operatore di copia = ().

Puoi avere molti costruttori! O finire con un sacco di costruttori in seguito. Ogni tanto Bill in la-la-land vuole un nuovo costruttore con float piuttosto che double per salvare quei 4 byte schifosi. (Compra un conto in RAM!)

Non è ansible chiamare il costruttore come se fosse ansible un metodo ordinario per ri-invocare quella logica di inizializzazione.

Non è ansible rendere virtuale la logica del costruttore e modificarla in una sottoclass. (Anche se si sta invocando un metodo initialize () dal costruttore piuttosto che manualmente, i metodi virtuali non funzioneranno.)

.

Tutte queste cose creano molto dolore quando esiste una logica significativa nel costruttore. (O almeno una duplicazione del codice.)

Quindi, come scelta progettuale, preferisco avere costruttori minimali che (opzionalmente, a seconda dei loro parametri e della situazione) invocano un metodo initialize ().

A seconda delle circostanze, initialize () può essere privato. Oppure può essere pubblico e supportare più invocazioni (ad es. Reinizializzazione).

.

In definitiva, la scelta qui varia in base alla situazione. Dobbiamo essere flessibili e considerare i compromessi. Non esiste una taglia unica.

L’approccio che useremmo per implementare una class con una singola istanza solitaria che utilizza thread per parlare con un pezzo di hardware dedicato e che deve essere scritto in 1/2 ora non è necessariamente ciò che useremmo per implementare una class rappresenta la matematica su numeri in virgola mobile a precisione variabile scritti in molti mesi.