Perché un ciclo while (true) in un costruttore è effettivamente negativo?


47

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

Qualcuno mi ha fatto una domanda interessante alla quale non è stato facile rispondere per me.

Perché (o lo è affatto?) Considerato come un cattivo design per consentire a un costruttore di una classe di iniziare un ciclo infinito (cioè un circuito di gioco)?

Ci sono alcuni concetti che sono rotti 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, il che è anche strano.
  • Tecnicamente tale classe non ha membri pubblici tranne il costruttore, il che rende più difficile la comprensione (specialmente per le lingue in cui non è disponibile alcuna implementazione)

E poi ci sono problemi tecnici:

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

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


61
Perché va bene? Se si sposta semplicemente il ciclo principale su un metodo (refactoring molto semplice), l'utente può scrivere un codice non sorprendente come questo:var g = new Game {...}; g.MainLoop();
Brandin

61
La tua domanda indica già 6 motivi per non usarlo e mi piacerebbe vederne uno solo a suo favore. Si potrebbe anche chiedere perché è una cattiva idea di mettere un while(true)loop in un setter di proprietà: new Game().RunNow = true?
Groo

38
Analogia estrema: perché diciamo che ciò che Hitler ha fatto era sbagliato? Tranne la discriminazione razziale, l'inizio della seconda guerra mondiale, l'uccisione di milioni di persone, la violenza [ecc ecc. Per oltre 50 altri motivi] non ha fatto nulla di male. Sicuramente se rimuovi un elenco arbitrario di motivi per cui qualcosa non va, puoi anche concludere che quella cosa è buona, praticamente per qualsiasi cosa.
Bakuriu,

4
Per quanto riguarda la garbage collection (GC) (il tuo problema tecnico), non ci sarebbe nulla di speciale al riguardo. L'istanza esiste prima dell'esecuzione effettiva del codice del costruttore. In questo caso l'istanza è ancora raggiungibile, quindi non può essere rivendicata da GC. Se il GC si attiva, questa istanza sopravvivrà e ad ogni occorrenza dell'impostazione del GC, l'oggetto verrà promosso alla generazione successiva secondo la normale regola. La presenza o meno di GC dipende dal fatto che (abbastanza) nuovi oggetti vengano creati o meno.
Jeppe Stig Nielsen,

8
Vorrei fare un ulteriore passo avanti e direi che while (true) è male, indipendentemente da dove viene utilizzato. Implica che non esiste un modo chiaro e pulito per interrompere il ciclo. Nel tuo esempio, il loop non dovrebbe fermarsi quando il gioco esce o perde concentrazione?
Jon Anderson,

Risposte:


250

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: costruire qualcosa.


11
Forse intendevano mettere mentre (vero) con una pausa nel mezzo? Rischiato, ma legittimo.
John Dvorak,

2
Sono d'accordo, questo è corretto - almeno. da un punto di vista accademico. Lo stesso vale per ogni funzione che restituisce qualcosa.
Doc Brown,

61
Immagina la povera zolla che crea una sottoclasse di detta classe ...
Newtopian,

5
@JanDvorak: Beh, questa è una domanda diversa. L'OP ha esplicitamente specificato "senza fine" e menzionato un ciclo di eventi.
Jörg W Mittag,

4
@JörgWMittag bene, quindi sì, non metterlo in un costruttore.
John Dvorak,

45

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

Sì, naturalmente. È inutile, inaspettato, inutile, non elegante. Infrange 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 mantenibile, i futuri programmatori passeranno molto tempo a cercare di capire cosa sta succedendo e provare a indovinare i motivi per cui è stato fatto in quel modo.

Niente di tutto ciò è un "bug", nel senso che il tuo codice non funziona. Ma probabilmente comporterà enormi costi secondari (rispetto al costo di scrittura del codice inizialmente) a lungo termine rendendo il codice più difficile da mantenere (ad esempio, test difficili da aggiungere, difficile da riutilizzare, difficile da eseguire il debug, difficile da estendere ecc. .).

Molti / più moderni miglioramenti nei metodi di sviluppo del software sono stati fatti specificamente per rendere più semplice l'effettivo processo di scrittura / test / debugging / manutenzione del software. Tutto ciò è eluso da cose come questa, in cui il codice viene inserito in modo casuale perché "funziona".

Sfortunatamente, incontrerai regolarmente programmatori che ignorano completamente tutto ciò. Funziona, tutto qui.

Per finire con un'analogia (un altro linguaggio di programmazione, qui il problema a portata di mano è 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ò fornire 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 trasformarlo in un metodo!)
  • Più lento (ma questo non è in un momento critico per il 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 dipendenze da bash(ma non funzionerà mai su Windows, Mac o Android)
  • E tutte le altre ragioni sopra menzionate.

5
"GC potrebbe trovarsi in uno stato di blocco eccezionale durante l'esecuzione di un costruttore" - perché? L'allocatore alloca per primo, quindi avvia il costruttore come un metodo ordinario. Non c'è niente di veramente speciale, vero? E l'allocatore deve consentire nuove allocazioni (e queste potrebbero innescare situazioni OOM) all'interno del costruttore comunque.
John Dvorak,

1
@JanDvorak, Volevo solo sottolineare che potrebbe esserci un comportamento speciale "da qualche parte" mentre si trova in un costruttore, ma hai ragione, l'esempio che ho scelto non era realistico. Rimosso.
AnoE

Mi piace molto la tua spiegazione e mi piacerebbe contrassegnare entrambe le risposte come risposte (avere almeno un voto). L'analogia è buona e lo è anche la tua scia di pensiero e spiegazione dei costi secondari, ma li considero requisiti ausiliari per il codice (come i miei argomenti). Lo stato dell'arte attuale è testare il codice, quindi la testabilità ecc. È un requisito di fatto. Ma la dichiarazione di @ JörgWMittag è perfetta fundamental contract of a constructor: to construct something.
Samuel,

L'analogia non è così buona. Se vedo questo mi aspetterei di vedere un commento da qualche parte sul perché stai usando Bash per fare i conti e, a seconda della tua ragione, potrebbe andare benissimo. Lo stesso non funzionerebbe per l'OP, perché praticamente dovresti scusarti nei commenti ovunque tu abbia usato il costruttore "// Nota: la costruzione di questo oggetto avvia un ciclo di eventi che non ritorna mai".
Brandin,

4
"Sfortunatamente, incontrerai regolarmente programmatori che sono completamente all'oscuro di tutto questo. Funziona, tutto qui." Così triste ma così vero. Penso di poter parlare per tutti noi quando dico che l'unico onere sostanziale nella nostra vita professionale è, beh, quelle persone.
Lightness Races with Monica

8

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

Il mio primo pensiero qui è stato che ciò 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 dietro a metterlo in un costruttore invece di un metodo normale. Poi mi è venuto in mente che l'unica differenza sarebbe che nel tuo loop potresti consentire ai riferimenti di thisfuggire parzialmente dal loop. Anche se questo non è strettamente limitato a questa situazione, è garantito che se si consente thisdi scappare, quei riferimenti indicheranno sempre un oggetto che non è completamente costruito.

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


Suppongo che il costruttore venga eseguito dopo che l'oggetto è stato creato, quindi in un linguaggio sano che perde questo comportamento dovrebbe essere perfettamente definito.
mroman,

@mroman: potrebbe essere ok trapelare thisnel costruttore - ma solo se sei nel costruttore della classe più derivata. Se hai due classi, Ae Bcon B : A, se il costruttore di Aperdesse this, i membri di Bsarebbero comunque non inizializzati (e il metodo virtuale che chiama o lancia il cast Bpuò provocare il caos in base a quello).
hoffmale,

1
Immagino che in C ++ possa essere pazzo, sì. In altre lingue i membri ricevono un valore predefinito prima che vengano assegnati i loro valori effettivi, quindi mentre è stupido scrivere effettivamente tale codice, il comportamento è ancora ben definito. Se chiami metodi che utilizzano questi membri, potresti imbatterti in NullPointerExceptions o calcoli errati (perché i numeri sono impostati su 0) o cose del genere, ma è tecnicamente deterministico cosa succede.
mroman l'

@mroman Riesci a trovare un riferimento definitivo per il CLR che dimostrerebbe che è così?
JimmyJames,

Bene, puoi assegnare i valori dei membri prima che il costruttore venga eseguito in modo che l'oggetto esista in uno stato valido con ai membri assegnati valori predefiniti o valori loro assegnati prima ancora che il costruttore fosse eseguito. Non è necessario il costruttore per inizializzare i membri. Fammi vedere se riesco a trovarlo da qualche parte nei documenti.
mroman,

1

+1 Per lo meno stupore, ma questo è un concetto difficile da articolare a nuovi sviluppatori. A livello pragmatico, è difficile eseguire il debug delle eccezioni sollevate all'interno dei costruttori, se l'oggetto non riesce a inizializzare non esisterà per te per ispezionare lo stato o accedere al di fuori di quel costruttore.

Se senti la necessità di fare questo tipo di pattern 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 si intende utilizzare l'oggetto che si sta "costruendo", qual è il punto di istanziare l'oggetto in primo luogo? Avere un ciclo (vero) in un costruttore in modo efficace significa che non hai mai intenzione di completarlo ...

C # è un linguaggio orientato agli oggetti molto ricco con molti costrutti e paradigmi diversi da esplorare, conoscere i propri strumenti e quando utilizzarli, linea di fondo:

In C # non eseguire logiche estese o infinite nei costruttori perché ... ci sono alternative migliori


1
il suo non sembra offrire nulla di sostanziale sui punti formulati e spiegati nelle precedenti risposte 9
moscerino

1
Ehi, ho dovuto provarlo :) Ho pensato che fosse importante sottolineare che, sebbene non ci sia alcuna regola contro questo, non dovresti in virtù del fatto che ci sono modi più semplici. La maggior parte delle altre risposte si concentra sul tecnicismo del perché è negativo, ma è difficile venderlo a un nuovo sviluppatore che dice "ma si compila, quindi posso lasciarlo così"
Chris Schaller,

0

Non c'è nulla di intrinsecamente negativo. Tuttavia, ciò che sarà importante è la domanda sul perché hai scelto di usare questo costrutto. Che cosa hai ottenuto facendo questo?

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

È molto comune in queste situazioni avere il costruttore semplicemente costruire l'oggetto e avere una funzione che chiama il ciclo infinito. Ciò offre all'utente maggiore flessibilità. Ad esempio, il tipo di problemi che possono essere visualizzati è la limitazione dell'uso dell'oggetto. Se qualcuno vuole avere un oggetto membro che è "l'oggetto di gioco" nella sua classe, deve essere pronto per eseguire l'intero ciclo di gioco nel proprio costruttore perché deve costruire l'oggetto. Ciò potrebbe portare alla codifica torturata per aggirare questo problema. In generale, non è possibile pre-allocare l'oggetto di gioco e quindi eseguirlo in un secondo momento. Per alcuni programmi non è un problema, ma ci sono classi di programmi in cui vuoi essere in grado di allocare tutto in anticipo e quindi chiamare il loop di gioco.

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

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


Se la costruzione del tuo oggetto richiede un loop di gioco, almeno inseriscilo in un metodo factory. GameTestResults testResults = runGameTests(tests, configuration);piuttosto che GameTestResults testResults = new GameTestResults(tests, configuration);.
user253751

@immibis Sì, è vero, tranne per i casi in cui ciò è irragionevole. Se stai lavorando con un sistema basato sulla riflessione che crea un'istanza dell'oggetto per te, potresti non avere la possibilità di creare un metodo di fabbrica.
Cort Ammon,

0

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

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

È possibile estrarre quel metodo di creazione e avvio del thread in una funzione a sé stante, al fine di separare la costruzione di oggetti e l'inizio effettivo del suo lavoro - Preferisco così perché ottengo un migliore controllo sull'accesso alle risorse.

Puoi anche dare un'occhiata al "Pattern di oggetti attivi". Alla fine, immagino che la domanda fosse puntata su quando iniziare il thread di un "oggetto attivo", e la mia preferenza è un disaccoppiamento di costruzione e iniziare per un migliore controllo.


0

Il tuo oggetto mi ricorda di iniziare le attività . L'oggetto operazione ha un costruttore, ma ci sono anche un sacco di metodi factory statici come: Task.Run, Task.Starte Task.Factory.StartNew.

Questi 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 afferma che infrange un contratto fondamentale , che suppongo significhi che Jörg è molto sorpreso. Sono d'accordo e non ho altro da aggiungere.

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

Puoi fornire costruttori che consentano un controllo accurato (come suggerisce @Brandin, qualcosa del genere var g = new Game {...}; g.MainLoop();, che accolgono gli utenti che non vogliono avviare immediatamente il gioco e magari vogliono passarlo prima. E puoi scrivere qualcosa come var runningGame = Game.StartNew();iniziare immediatamente facile.


Significa che Jörg è fondamentalmente sorpreso, vale a dire una sorpresa del genere è un dolore nel fondo.
Steve Jessop,

0

Perché un ciclo while (true) in un costruttore è effettivamente negativo?

Non è affatto male.

Perché (o lo è affatto?) Considerato come un cattivo design per consentire a un costruttore di una classe di iniziare un ciclo infinito (cioè un circuito di gioco)?

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

Hai menzionato alcune delle ovvie ragioni contro di essa, ma hai lasciato fuori 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 quanto sia meglio, hai scelto quella migliore. Per inciso, ecco perché non usiamo quasi mai GoTo.


-1

Lo scopo di un costruttore è, bene, contrarre il tuo oggetto. Prima che il costruttore sia completato, in linea di principio non è sicuro utilizzare alcun metodo di quell'oggetto. Naturalmente, possiamo 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 attuale stato di mezzo forno.

Mettendo indirettamente tutta la logica nel costruttore, aggiungi più peso di questo tipo su te stesso. Questo suggerisce che non hai progettato la differenza tra l'inizializzazione e l'uso abbastanza bene.


1
questo non sembra offrire nulla di sostanziale rispetto ai punti formulati e spiegati nelle precedenti 8 risposte
moscerino del
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.