Perché un loop while (true) in un costruttore è effettivamente cattivo?

47

Anche se una domanda generale il mio ambito è piuttosto C #, sono consapevole che linguaggi come il C ++ hanno semantica diversa per quanto riguarda l'esecuzione del costruttore, la gestione della memoria, il comportamento indefinito, ecc.

Qualcuno mi ha fatto una domanda interessante che per me non è stata una risposta facile.

Perché (o lo è per niente?) considerato un cattivo design per consentire a un costruttore di una classe di avviare un ciclo infinito (ad es. il loop di gioco)?

Ci sono alcuni concetti che sono infranti da questo:

  • come il principio del minimo stupore, l'utente non si aspetta che il costruttore si comporti in questo modo.
  • I test unitari sono più difficili in quanto non è possibile creare questa classe o iniettarla poiché non esce mai dal ciclo.
  • La fine del ciclo (fine del gioco) è quindi concettualmente il momento in cui termina il costruttore, che è anche dispari.
  • Tecnicamente una classe di questo tipo non ha membri pubblici tranne il costruttore, il che rende più difficile da capire (specialmente per le lingue in cui non è disponibile alcuna implementazione)

E poi ci sono problemi tecnici:

  • Il costruttore non finisce mai, quindi cosa succede con GC qui? Questo oggetto è già in Gen 0?
  • Il derivare da una classe di questo tipo è impossibile o almeno molto complicato a causa del fatto che il costruttore di base non restituisce mai

C'è qualcosa di più evidentemente cattivo o subdolo con un simile approccio?

    
posta Samuel 08.09.2016 - 11:04
fonte

9 risposte

249

Qual è lo scopo di un costruttore? Restituisce un oggetto di nuova costruzione. Cosa fa un ciclo infinito? Non ritorna mai. Come può il costruttore restituire un oggetto di nuova costruzione se non ritorna affatto? Non può.

Ergo, un ciclo infinito rompe il contratto fondamentale di un costruttore: per costruire qualcosa.

    
risposta data 08.09.2016 - 12:59
fonte
44

Is there something more obviously bad or devious with such an approach?

Sì, certo. È inutile, inaspettato, inutile, poco elegante. Viola i concetti moderni di design di classe (coesione, accoppiamento). Rompe il contratto del metodo (un costruttore ha un lavoro definito e non è solo un metodo casuale). Non è certamente ben gestibile, i futuri programmatori impiegheranno molto tempo a cercare di capire cosa sta succedendo e cercando di indovinare i motivi per cui è stato fatto in quel modo.

Niente di ciò è un "bug" nel senso che il tuo codice non funziona. Tuttavia, a lungo termine, si incorre in enormi costi secondari (rispetto al costo di scrittura del codice) rendendo il codice più difficile da mantenere (ad esempio, difficile aggiungere test, difficile da riutilizzare, difficile da debug, difficile da estendere ecc. .).

Molti / più moderni miglioramenti nei metodi di sviluppo del software sono fatti specificamente per rendere più semplice l'effettivo processo di scrittura / test / debug / manutenzione del software. Tutto questo viene aggirato da cose come questa, dove il codice viene inserito casualmente perché "funziona".

Sfortunatamente, incontrerai regolarmente programmatori che sono completamente all'oscuro di tutto ciò. Funziona, è così.

Per finire con un'analogia (un altro linguaggio di programmazione, qui il problema è calcolare 2+2 ):

$sum_a = 'bash -c "echo $((2+2)) 2>/dev/null"';   # calculate
chomp $sum_a;                         # remove trailing \n
$sum_a = $sum_a + 0;                  # force it to be a number in case some non-digit characters managed to sneak in

$sum_b = 2+2;

Cosa c'è di sbagliato nel primo approccio? Restituisce 4 dopo un periodo di tempo ragionevolmente breve; è corretto. Le obiezioni (insieme a tutti i soliti motivi che uno sviluppatore può dare per confutarle) sono:

  • Più difficile da leggere (ma hey, un buon programmatore può leggerlo con la stessa facilità, e lo abbiamo sempre fatto in questo modo, se vuoi posso rifarlo in un metodo!)
  • Più lento (ma questo non si trova in un punto critico del tempo, quindi possiamo ignorarlo, e inoltre, è meglio ottimizzare solo quando necessario!)
  • Utilizza molte più risorse (un nuovo processo, più RAM ecc. - ma dito, il nostro server è più che abbastanza veloce)
  • Introduce le dipendenze su bash (ma non funzionerà mai su Windows, Mac o Android comunque)
  • E tutti gli altri motivi sopra menzionati.
risposta data 08.09.2016 - 18:12
fonte
8

Fornisci sufficienti motivi nella tua domanda per escludere questo approccio, ma la vera domanda qui è "C'è qualcosa di più evidentemente cattivo o subdolo con un simile approccio?"

Il mio primo pensiero è che non ha senso. Se il tuo costruttore non finisce mai, nessun'altra parte del programma può ottenere un riferimento all'oggetto costruito , quindi qual è la logica che sta dietro di metterlo in un costruttore invece di un metodo normale. Poi mi è venuto in mente che l'unica differenza sarebbe che nel tuo ciclo potevi permettere riferimenti a this di fuga parzialmente costruita dal ciclo. Anche se questo non è strettamente limitato a questa situazione, è garantito che se permetti a this di uscire, quei riferimenti punteranno sempre a un oggetto che non è completamente costruito.

Non so se la semantica di questo tipo di situazione sia ben definita in C #, ma direi che non ha importanza perché non è qualcosa che la maggior parte degli sviluppatori vorrebbe provare a approfondire.

    
risposta data 08.09.2016 - 18:58
fonte
1

+1 Per lo meno stupore, ma questo è un concetto difficile da articolare con i nuovi sviluppatori. A livello pragmatico, è difficile eseguire il debug delle eccezioni sollevate all'interno dei costruttori, se l'oggetto non riesce a inizializzare, non esiste per voi per ispezionare lo stato di o il log dall'esterno del costruttore.

Se senti la necessità di fare questo tipo di schema di codice, usa invece metodi statici sulla classe.

Esiste un costruttore per fornire la logica di inizializzazione quando un oggetto viene istanziato da una definizione di classe. Costruisci questa istanza di oggetto perché è un contenitore che incapsula un insieme di proprietà e funzionalità che desideri chiamare dal resto della logica dell'applicazione.

Se non intendi usare l'oggetto che stai "costruendo", allora qual è il punto di istanziare l'oggetto in primo luogo? Avere un ciclo while (true) in un costruttore in modo efficace significa che non si intende mai per il completamento ...

C # è un linguaggio orientato agli oggetti molto ricco con molti costrutti e paradigmi diversi da esplorare, conoscere i tuoi strumenti e quando usarli, bottom line:

In C# do not execute extended or never-ending logic within constructors because... there are better alternatives

    
risposta data 10.09.2016 - 16:37
fonte
0

Non c'è niente di intrinsecamente male in questo. Tuttavia, ciò che sarà importante è la domanda sul perché si è scelto di utilizzare questo costrutto. Cosa hai ottenuto facendo questo?

Prima di tutto, non parlerò dell'uso di while(true) in un costruttore. C'è assolutamente niente di sbagliato nell'usare quella sintassi in un costruttore. Tuttavia, il concetto di "avvio di un ciclo di gioco nel costruttore" è uno che può causare alcuni problemi.

È molto comune in queste situazioni che il costruttore costruisca semplicemente l'oggetto e abbia una funzione che chiama il ciclo infinito. Ciò offre all'utente una maggiore flessibilità del codice. Come esempio dei tipi di problemi che possono emergere è che si limita l'uso del proprio oggetto. Se qualcuno vuole avere un oggetto membro che è "l'oggetto del gioco" sulla loro classe, devono essere pronti a eseguire l'intero ciclo di gioco nel loro costruttore perché devono costruire l'oggetto. Ciò potrebbe portare a codifica torturata per aggirare questo problema. Nel caso generale, non è possibile pre-allocare il proprio oggetto di gioco e quindi eseguirlo in un secondo momento. Per alcuni programmi questo non è un problema, ma ci sono classi di programmi in cui vuoi essere in grado di allocare tutto in anticipo e quindi chiamare il ciclo di gioco.

Una delle regole empiriche nella programmazione è usare lo strumento più semplice per il lavoro. Non è una regola dura e veloce, ma è molto utile. Se una chiamata di funzione sarebbe stata sufficiente, perché preoccuparsi di costruire un oggetto di classe? Se vedo uno strumento più potente e complicato, suppongo che lo sviluppatore abbia una ragione per questo, e inizierò a esplorare il tipo di trucchi che potresti fare.

Ci sono casi in cui ciò potrebbe essere ragionevole. Ci possono essere casi in cui è veramente intuitivo per te avere un ciclo di gioco nel costruttore. In quei casi, corri con esso! Ad esempio, il tuo ciclo di gioco può essere incorporato in un programma molto più grande che costruisce classi per incorporare alcuni dati. Se devi eseguire un ciclo di gioco per generare quei dati, potrebbe essere molto ragionevole eseguire il ciclo di gioco in un costruttore. Consideralo come un caso speciale: sarebbe valido farlo perché il programma globale rende intuitivo per il prossimo sviluppatore capire cosa hai fatto e perché.

    
risposta data 08.09.2016 - 21:21
fonte
0

La mia impressione è che alcuni concetti siano confusi e abbiano portato a una domanda formulata male.

Il costruttore di una classe può iniziare un nuovo thread che "mai" finirà (anche se preferisco usare while(_KeepRunning) su while(true) perché posso settare il membro booleano su false da qualche parte).

Potresti estrarre quel metodo di creare e avviare il thread in una sua funzione, al fine di separare la costruzione dell'oggetto e l'effettivo inizio del suo lavoro - Preferisco in questo modo perché ottengo un controllo migliore sull'accesso alle risorse.

Si potrebbe anche dare un'occhiata al "Modello oggetto attivo". Alla fine, immagino che la domanda fosse mirata a quando iniziare il thread di un "oggetto attivo", e la mia preferenza è un disaccoppiamento di costruzione e avvio per un controllo migliore.

    
risposta data 09.09.2016 - 09:07
fonte
0

Il tuo oggetto mi ricorda di avviare Attività . L'oggetto compito ha un costruttore, ma ci sono anche alcuni metodi statici di fabbrica come: Task.Run , Task.Start e Task.Factory.StartNew .

Sono molto simili a quello che stai cercando di fare, ed è probabile che questa sia la 'convenzione'. Il problema principale che la gente sembra avere è che questo uso di un costruttore li sorprende. @ JörgWMittag dice che interrompe un contratto fondamentale , che suppongo significhi che Jörg sia molto sorpreso. Sono d'accordo e non ho altro da aggiungere a questo.

Tuttavia, voglio suggerire che l'OP prova i metodi statici di fabbrica. La sorpresa svanisce e non c'è alcun contratto fondamentale in gioco qui. Le persone sono abituate a metodi statici di fabbrica che fanno cose speciali e possono essere nominati di conseguenza.

Puoi fornire costruttori che consentono un controllo a grana fine (come suggerisce @Bandand, qualcosa come var g = new Game {...}; g.MainLoop(); , che accontenta gli utenti che non vogliono iniziare immediatamente il gioco e forse vogliono passarlo prima per primo. scrivi qualcosa come var runningGame = Game.StartNew(); per renderlo immediatamente facile.

    
risposta data 09.09.2016 - 14:54
fonte
0

Why is a while(true) loop in a constructor actually bad?

Non è affatto male.

Why (or is it at all?) regarded as bad design to let a constructor of a class start a never ending loop (i.e. game loop)?

Perché vorresti mai farlo? Sul serio. Quella classe ha una singola funzione: il costruttore. Se tutto ciò che vuoi è una singola funzione, è per questo che abbiamo funzioni, non costruttori.

Hai menzionato alcune delle ovvie ragioni contro di te stesso, ma hai tralasciato il più grande:

Ci sono opzioni migliori e più semplici per ottenere la stessa cosa.

Se ci sono 2 scelte e una è migliore, non importa in quanto è meglio, hai scelto quella migliore. Per inciso, è per questo che non non usiamo quasi mai GoTo.

    
risposta data 12.09.2016 - 13:05
fonte
-1

Lo scopo di un costruttore è, beh, contrarre il tuo oggetto. Prima che il costruttore sia completato, è in linea di principio non sicuro utilizzare qualsiasi metodo di quell'oggetto. Naturalmente, potremmo chiamare metodi dell'oggetto all'interno del costruttore, ma ogni volta che lo facciamo, dobbiamo assicurarci che farlo sia valido per l'oggetto nel suo stato attuale mezzo-baken.

Inserendo indirettamente tutta la logica nel costruttore, aggiungi altro carico di questo tipo a te stesso. Questo suggerisce che non hai progettato la differenza tra l'inizializzazione e l'uso abbastanza bene.

    
risposta data 10.09.2016 - 11:22
fonte

Leggi altre domande sui tag