Quando è accettabile un riferimento circolare a un puntatore genitore?


24

Questa domanda Stack Overflow riguarda un bambino che ha riferimento al suo genitore, tramite un puntatore.

Inizialmente i commenti erano piuttosto critici sul fatto che il design fosse un'idea orribile.

Capisco che questa non è probabilmente la migliore idea in generale. Da una regola empirica generale sembra giusto dire "non farlo!"

Tuttavia, mi chiedo che tipo di condizioni esisterebbero dove dovresti fare qualcosa del genere. Questa domanda qui e le risposte / i commenti associati suggeriscono anche che i grafici non facciano qualcosa del genere.


1
La domanda che hai collegato sembra piuttosto esauriente sull'argomento.
Lightness Races con Monica

4
@LightnessRacesinOrbit "non farlo" non è molto utile per capire il perché .
Enderland

2
Vedo molto più di "non farlo" lì. Vedo pro e contro discussi da più esperti.
Lightness Races con Monica

1
Potresti avere un elenco bidirezionale che deve essere attraversato, una sorta di buffer circolare, forse stai rappresentando due pezzi di strada collegata in un gioco - se devi rappresentare qualcosa di circolare, allora questa potrebbe essere una buona idea.
rhughes

2
La mia pragmatica regola del pollice è una domanda "Può un bambino esistere senza un genitore?". (Se si considerano XmlDocuments e i suoi nodi: un nodo non può esistere senza il contesto dell'albero di un documento. È una sciocchezza). Se la risposta è no, allora i collegamenti bidirezionali vanno bene: hai due oggetti che possono esistere solo insieme. Se gli oggetti possono esistere in modo indipendente, rimuovo uno di quei due collegamenti.
Mikalai,

Risposte:


43

La chiave qui non è se due oggetti hanno riferimenti circolari, ma se quei riferimenti indicano la proprietà reciproca.

Due oggetti non possono "possedere" l'un l'altro: questo provoca un dilemma intrattabile per l'inizializzazione e l'ordine di cancellazione. Uno deve essere un riferimento facoltativo o indicare altrimenti che un oggetto non gestirà la vita dell'altro.

Considera un elenco doppiamente collegato: due nodi si collegano avanti e indietro, ma nessuno dei due "possiede" l'altro (l'elenco possiede entrambi). Ciò significa che nessuno dei due nodi alloca memoria per l'altro o è altrimenti responsabile dell'identità o della gestione a vita dell'altro.

Gli alberi hanno una relazione simile, anche se i nodi in un albero possono allocare figli e i genitori possiedono figli. Il collegamento da figlio a genitore aiuta con l'attraversamento, ma di nuovo non definisce la proprietà.

Nella maggior parte dei progetti OO, un riferimento a un altro oggetto come membro dei dati di un oggetto implica la proprietà. Ad esempio, supponiamo di avere classi Car and Engine. Nessuno dei due è molto utile da solo. Possiamo dire che questi oggetti dipendono l'uno dall'altro: richiedono la presenza dell'altro per svolgere un lavoro utile. Ma quale "possiede" l'altro? In questo caso, diremmo che Car possiede il motore perché l'auto è il "contenitore" in cui vivono tutti i componenti automobilistici. Sia in un OO che nel design del mondo reale, l'auto è la somma delle sue parti e tutte quelle parti sono collegate insieme nel contesto dell'auto. Il motore potrebbe avere un riferimento a Auto o potrebbe avere un riferimento a TorqueConverter,

I riferimenti circolari possono essere un cattivo odore di design, ma non necessariamente. Se utilizzati con giudizio e documentati correttamente, possono semplificare l'utilizzo delle strutture dati.

Prova a attraversare un albero senza riferimenti andando in entrambe le direzioni tra genitori e figli. Certo, potresti trovare un approccio basato sullo stack che è fragile e complesso, oppure potresti usare un approccio basato sul riferimento che è banalmente semplice.


16

Ci sono diversi aspetti da considerare in un tale progetto:

  • le dipendenze strutturali
  • la relazione di proprietà (iecomposizione vs. altro tipo di associazione)
  • le esigenze di navigazione

Dipendenza strutturale tra le classi:

Se si intende riutilizzare le classi di componenti, è necessario evitare dipendenze non necessarie ed evitare strutture circolari chiuse.

Tuttavia a volte due classi sono concettualmente fortemente interconnesse. In questo caso, evitare la dipendenza non è una vera opzione. Esempio: un albero e le sue foglie, o più in generale un composto e i suoi componenti .

Proprietà degli oggetti:

Un oggetto possiede l'altro? O altrimenti indicato: se un oggetto viene distrutto, anche l'altro deve essere distrutto?

Questo argomento è stato approfondito da Snowman, quindi non affronterò qui.

Esigenze di navigazione tra oggetti:

Un ultimo problema è la necessità di navigazione. Facciamo il mio esempio preferito, il modello di design composito della Gang of four .

Gamma & al. menzionare esplicitamente la potenziale necessità di avere un riferimento genitore esplicito: " Mantenere il riferimento dai componenti figlio ai loro genitori può semplificare l'attraversamento e la gestione di una struttura composita " Naturalmente si potrebbe immaginare un attraversamento sistematico dall'alto verso il basso, ma per oggetti compositi molto grandi può rallentare significativamente le operazioni e in modo esponenziale. Un riferimento diretto, anche circolare, può facilitare in modo significativo la manipolazione dei compositi.

Un esempio potrebbe essere un modello grafico di un sistema elettronico. Una struttura composita potrebbe rappresentare le schede elettroniche, i circuiti, gli elementi. Per visualizzare e manipolare il modello, sono necessari alcuni proxy geometrici in una vista della GUI. È quindi molto più semplice navigare dall'elemento GUI selezionato dall'utente al componente, scoprire qual è il genitore e con quali sono gli elementi fratello / sorella correlati, piuttosto che avviare una ricerca dall'alto verso il basso.

Naturalmente, come ha sottolineato Gamma & al, devi garantire gli invarianti della relazione circolare. Questo può essere difficile, come ha mostrato la domanda SO a cui ti riferisci. Ma è perfettamente gestibile e in modo sicuro.

Conclusione

La necessità di navigazione non deve essere sottovalutata. Non è senza ragione che UML lo abbia esplicitamente affrontato nella notazione di modellazione. E sì, ci sono situazioni perfettamente valide in cui sono necessari riferimenti circolari.

L'unico punto è che a volte le persone tendono ad andare in tale direzione rapidamente. Quindi vale la pena considerare tutti e 3 gli aspetti coinvolti prima di prendere la decisione di farlo o no.


1
Questa risposta è IMHO fortemente non riservata. Il PO ha chiesto: "Dato che esiste già una relazione genitore-figlio, quando va bene implementarlo con un riferimento circolare". Quindi la struttura e la "proprietà" (nel senso qui menzionato) sono già chiare. Ciò significa che gli unici criteri per aggiungere riferimenti da una parte o dall'altra sono le domande relative ai "bisogni di navigazione" e al "riutilizzo indipendente del minore".
Doc Brown,

@DocBrown - Puoi sempre dare una taglia a questa domanda una volta che diventa ammissibile. :-)

1
@ GlenH7: questo non darebbe a questa risposta più voti, solo la risposta di Snowman (per la quale penso che manchi un po 'il punto della domanda).
Doc Brown,

1
@DocBrown - Ma il repz, amico, il repz!

... e se aggiungi opzioni di visibilità come pubblico-privato-protetto, questo ti dà 9 diverse opzioni :)
mikalai

8

Di solito, i riferimenti circolari sono una pessima idea perché significano dipendenze circolari. Probabilmente sai già perché le dipendenze circolari sono cattive, ma per completezza, la versione tl; dr è che ogni volta che le classi A e B dipendono l'una dall'altra, è impossibile capire / correggere / ottimizzare / ecc. A o B senza anche comprendere / correggere / ottimizzare / ecc. l'altra classe contemporaneamente. Ciò porta rapidamente a basi di codice in cui non è possibile cambiare nulla senza cambiare tutto.

Tuttavia, è possibile avere un riferimento circolare senza creare dipendenze circolari malvagie. Funziona fintanto che il riferimento è strettamente facoltativo in senso funzionale. Con ciò voglio dire che potresti facilmente rimuoverlo dalle classi e funzionerebbero comunque, anche se succedono più lentamente. Il caso d'uso principale di cui sono a conoscenza per tali riferimenti circolari che creano non dipendenze è consentire l'attraversamento rapido di strutture di dati basate su nodi come elenchi collegati, alberi e cumuli. Ad esempio, in linea di principio qualsiasi operazione che puoi eseguire su un elenco doppiamente collegato che puoi anche eseguire su un elenco collegato singolarmente, ci sono solo alcune operazioni (come spostarti indietro nell'elenco) che hanno un grande molto meglio O con la versione doppiamente collegata.


6

La ragione per cui di solito non è una buona idea farlo è perché viola il principio di inversione di dipendenza . La gente ha scritto molto su questo in modo molto più dettagliato di quello che posso trattare adeguatamente in questo post, ma si riduce a rendere difficile la sua manutenzione, perché l'accoppiamento è così stretto. Cambiare una classe quasi sempre richiede un cambiamento nell'altra, mentre se le dipendenze puntano solo in una direzione, le modifiche su un lato dell'interfaccia sono isolate. Se entrambe le classi indicano un'interfaccia astratta, ancora meglio.

L'unica eccezione principale è quando non hai due classi diverse a diversi livelli di astrazione, ma due nodi della stessa classe, come in un albero, un elenco doppiamente collegato, ecc. Qui è più una relazione strutturale che un relazione di astrazione. Riferimenti circolari per motivi di efficienza algoritmica sono accettabili e persino incoraggiati in questo tipo di casi.


5

[...] suggerisce anche ai grafici di non fare qualcosa del genere.

A volte basta accedere alle cose dal basso verso l'alto da una struttura di dati diversa rispetto all'albero, mentre l'albero deve accedere alle cose in modo discendente.

Ad esempio, un quadtree potrebbe memorizzare elementi in un software di grafica vettoriale. Tuttavia, la selezione dell'utente viene memorizzata in un elenco di selezione separato di riferimenti / puntatori di elementi vettoriali. Quando l'utente vuole eliminare quella selezione, dobbiamo aggiornare il quadtree, e lì potrebbe essere molto più efficiente aggiornare l'albero in modo bottom-up a partire dalle foglie piuttosto che dall'alto in basso. Altrimenti dovresti, per ogni elemento, lavorare da radice a foglia e quindi eseguire nuovamente il backup.


1
Sì, questo esempio è davvero appropriato. Esattamente il tipo di cose che stavo cercando di descrivere nella mia discussione sulla navigazione in un composito: il backpointer accelera considerevolmente la velocità. Grazie per il tuo interessante aneddoto sulle prestazioni e un diagramma molto chiaro! +1
Christophe,

@Christophe Mi piace anche il tuo esempio! Non ero sicuro se stavo davvero rispondendo correttamente alla domanda poiché forse si trattava più di "proprietà circolare" che di semplici puntatori che ci permettevano di attraversare una struttura di dati verso l'alto / all'indietro. Ma stavo principalmente rispondendo a quell'ultimo "grafico" della domanda.

2

Doom 3 ha un esempio di un oggetto figlio con un puntatore a un oggetto genitore. In particolare utilizza elenchi invadenti . Per riassumere, un elenco intrusivo è come un elenco collegato, tranne per il fatto che ciascun nodo contiene un puntatore all'elenco stesso.

vantaggi:

  • Quando gli oggetti possono esistere in più elenchi contemporaneamente, la memoria per i nodi elenco deve essere allocata e deallocata una sola volta.

  • Quando un oggetto deve essere distrutto, puoi rimuoverlo facilmente da tutti gli elenchi in cui si trova senza cercare in modo lineare ciascun elenco.

Penso che questo sia uno scenario piuttosto specifico, ma se capisco la tua domanda, è un esempio di un uso accettabile di un oggetto figlio contenente un puntatore al suo oggetto padre.

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.