Quando vorresti due riferimenti allo stesso oggetto?


20

In Java in particolare, ma probabilmente anche in altre lingue: quando sarebbe utile avere due riferimenti allo stesso oggetto?

Esempio:

Dog a = new Dog();
Dob b = a;

C'è una situazione in cui ciò sarebbe utile? Perché questa sarebbe una soluzione preferita da usare aogni volta che vuoi interagire con l'oggetto rappresentato da a?


È l'essenza dietro il modello Flyweight .

@MichaelT Potresti elaborare?
Bassinator

7
Se ae fai b sempre riferimento allo stesso Dog, non ha senso. Se a volte potrebbero, allora è abbastanza utile.
Gabe,

Risposte:


45

Un esempio è quando si desidera avere lo stesso oggetto in due elenchi separati :

Dog myDog = new Dog();
List dogsWithRabies = new ArrayList();
List dogsThatCanPlayPiano = new ArrayList();

dogsWithRabies.add(myDog);
dogsThatCanPlayPiano.add(myDog);
// Now each List has a reference to the same dog

Un altro uso è quando hai lo stesso oggetto che gioca diversi ruoli :

Person p = new Person("Bruce Wayne");
Person batman = p;
Person ceoOfWayneIndustries = p;

4
Mi dispiace ma credo che per il tuo esempio di Bruce Wayne ci sia un difetto di progettazione, la posizione di Batman e CEO dovrebbe essere un ruolo per la tua persona.
Silviu Burcea,

44
-1 per rivelare l'identità segreta di Batman.
Appunto

2
@Silviu Burcea - Sono pienamente d'accordo che l'esempio di Bruce Wayne non sia un buon esempio. Da un lato, se una modifica globale del testo ha cambiato il nome 'ceoOfWayneIndustries' e 'batman' in 'p' (supponendo che non vi siano scontri di nome o cambiamenti di portata) e che la semantica del programma sia cambiata, allora la loro è qualcosa di rotto. L'oggetto a cui si fa riferimento rappresenta il significato reale, non il nome della variabile all'interno del programma. Per avere una semantica diversa, è un oggetto diverso o è referenziato da qualcosa con più comportamento di un riferimento (che dovrebbe essere trasparente), e quindi non è un esempio di 2+ riferimenti.
bagliore

2
Sebbene l'esempio di Bruce Wayne come scritto potrebbe non funzionare, credo che l' intenzione dichiarata sia corretta. Forse un esempio più vicino potrebbe essere Persona batman = new Persona("Batman"); Persona bruce = new Persona("Bruce Wayne"); Persona currentPersona = batman;: dove hai più valori possibili (o un elenco di valori disponibili) e un riferimento a quello attualmente attivo / selezionato.
Dan Puzey,

1
@gbulmer: direi che non puoi avere un riferimento a due oggetti; il riferimento currentPersonapunta a un oggetto o all'altro, ma mai entrambi. In particolare, potrebbe essere facilmente possibile che noncurrentPersona sia mai impostato bruce, nel qual caso non è certamente un riferimento a due oggetti. Direi che, nel mio esempio, entrambi batmane currentPersonasono riferimenti alla stessa istanza, ma servono un significato semantico diverso all'interno del programma.
Dan Puzey,

16

Questa è in realtà una domanda sorprendentemente profonda! L'esperienza del C ++ moderno (e dei linguaggi che derivano dal C ++ moderno, come Rust) suggerisce che molto spesso non lo si desidera! Per la maggior parte dei dati, si desidera un riferimento singolo o univoco ("proprietario"). Questa idea è anche la ragione principale per i sistemi di tipo lineare .

Tuttavia, anche in questo caso si desidera in genere alcuni riferimenti "prestati" di breve durata che vengono utilizzati per accedere brevemente alla memoria ma non durano per una frazione significativa del tempo in cui i dati esistono. Più comunemente quando si passa un oggetto come argomento a un'altra funzione (anche i parametri sono variabili!):

void encounter(Dog a) {
  hissAt(a);
}

void hissAt(Dog b) {
  // ...
}

Un caso meno comune quando si utilizza uno di due oggetti a seconda di una condizione, facendo essenzialmente la stessa cosa indipendentemente da quale si sceglie:

Dog a, b;
Dog goodBoy = whoseAGoodBoy ? a : b;
feed(goodBoy);
walk(goodBoy);
pet(goodBoy);

Tornando agli usi più comuni, ma lasciando indietro le variabili locali, passiamo ai campi: ad esempio, i widget nei framework GUI hanno spesso widget principali, quindi il tuo grande frame contenente dieci pulsanti avrebbe almeno dieci riferimenti che puntano su di esso (più alcuni altri dai suoi genitori e forse dagli ascoltatori di eventi e così via). Qualsiasi tipo di grafico a oggetti e alcuni tipi di alberi di oggetti (quelli con riferimenti padre / fratello), hanno più oggetti che fanno riferimento a ciascuno lo stesso oggetto. E praticamente ogni set di dati è in realtà un grafico ;-)


3
Il "caso meno comune" è abbastanza comune: hai gli oggetti memorizzati in un elenco o in una mappa, recupera quello che desideri ed esegui le operazioni richieste. Non si cancellano i loro riferimenti dalla mappa o dall'elenco.
SJuan76,

1
@ SJuan76 Il "caso meno comune" riguarda esclusivamente il prendere dalle variabili locali, tutto ciò che riguarda le strutture di dati rientra nell'ultimo punto.

Questo ideale di un solo riferimento è dovuto alla gestione della memoria per il conteggio dei riferimenti? In tal caso, varrebbe la pena ricordare che la motivazione non è rilevante per altre lingue con diversi garbage collector (ad es. C #, Java, Python)
MarkJ

@MarkJ I tipi lineari non sono solo un codice ideale, molto esistente, inconsapevolmente si conforma ad esso perché è la cosa semanticamente corretta per parecchi casi. Presenta potenziali vantaggi in termini di prestazioni (ma per il conteggio dei riferimenti né per la tracciabilità dei GC, aiuta solo quando si utilizza una strategia diversa che sfrutta effettivamente tale conoscenza per omettere sia i conteggi che la traccia). Più interessante è la sua applicazione alla gestione delle risorse semplice e deterministica. Pensa che RAII e C ++ 11 spostino la semantica, ma meglio (si applica più spesso e gli errori vengono colti dal compilatore).

6

Variabili temporanee: considerare il seguente pseudocodice.

Object getMaximum(Collection objects) {
  Object max = null;
  for (Object candidate IN objects) {
    if ((max is null) OR (candidate > max)) {
      max = candidate;
    }
  }
  return max;
}

Le variabili maxe candidatepossono puntare allo stesso oggetto, ma l'assegnazione delle variabili cambia usando regole diverse e in momenti diversi.


3

Per integrare le altre risposte, potresti anche voler attraversare una struttura di dati in modo diverso, a partire dallo stesso posto. Ad esempio, se avessi un BinaryTree a = new BinaryTree(...); BinaryTree b = a, potresti attraversare il percorso più a sinistra dell'albero con ae il suo percorso più a destra con b, usando qualcosa come:

while (!a.equals(null) && !b.equals(null)) {
    a = a.left();
    b = b.right();
}

È da un po 'che non scrivo Java, quindi il codice potrebbe non essere corretto o ragionevole. Prendilo di più come pseudocodice.


3

Questo metodo è eccezionale quando si hanno diversi oggetti che richiamano tutti su un altro oggetto che può essere utilizzato in modo non contestuale.

Ad esempio, se si dispone di un'interfaccia a schede, è possibile che siano presenti Tab1, Tab2 e Tab3. Potresti anche voler essere in grado di utilizzare una variabile comune indipendentemente da quale scheda si trova l'utente per semplificare il codice e ridurre la necessità di capire al volo più e più volte su quale scheda si trova l'utente.

Tab Tab1 = new Tab();
Tab Tab2 = new Tab();
Tab Tab3 = new Tab();
Tab CurrentTab = new Tab();

Quindi, in ciascuna delle schede numerate di Click, è possibile modificare CurrentTab per fare riferimento a quella scheda.

CurrentTab = Tab3;

Ora nel tuo codice puoi chiamare "CurrentTab" con impunità senza bisogno di sapere su quale scheda ti trovi effettivamente. È inoltre possibile aggiornare le proprietà di CurrentTab che scorreranno automaticamente verso il basso fino alla scheda di riferimento.


3

Ci sono molti scenari in cui b deve essere un riferimento a un " a" sconosciuto per essere utile. In particolare:

  • Ogni volta che non sai cosa bindica in fase di compilazione.
  • Ogni volta che è necessario scorrere su una raccolta, sia essa nota in fase di compilazione o meno
  • Ogni volta che hai un ambito limitato

Per esempio:

parametri

public void DoSomething(Thing &t) {
}

t è un riferimento a una variabile da un ambito esterno.

Restituisce valori e altri valori condizionali

Thing a = Thing.Get("a");
Thing b = Thing.Get("b");
Thing biggerThing = Thing.max(a, b);
Thing z = a.IsMoreZThan(b) ? a : b;

biggerThinge zsono entrambi riferimenti a ao b. Non sappiamo quale al momento della compilazione.

Lambdas e i loro valori di ritorno

Thing t = someCollection.FirstOrDefault(x => x.whatever > 123);

xè un parametro (esempio 1 sopra) ed tè un valore di ritorno (esempio 2 sopra)

collezioni

indexByName.add(t.name, t);
process(indexByName["some name"]);

index["some name"]è, in larga misura, un aspetto più sofisticato b. È un alias per un oggetto che è stato creato e inserito nella collezione.

Loops

foreach (Thing t in things) {
 /* `t` is a reference to a thing in a collection */
}

t è un riferimento a un articolo restituito (esempio 2) da un iteratore (esempio precedente).


I tuoi esempi sono difficili da seguire. Non fraintendetemi, li ho superati, ma ho dovuto lavorarci su. In futuro, suggerisco di separare i vostri esempi di codice in singoli blocchi (con alcune note che spiegano specificamente ciascuno di essi), senza mettere alcuni esempi non correlati in un blocco.
Bassinator

1
@HCBPshenanigans Non mi aspetto che tu cambi le risposte selezionate; ma ho aggiornato il mio per aiutare, si spera, a facilitare la leggibilità e compilare alcuni dei casi d'uso mancanti nella risposta selezionata.
svidgen,

2

È un punto cruciale, ma IMHO merita di essere compreso.

Tutte le lingue OO fanno sempre copie di riferimenti e non copiano mai un oggetto "invisibilmente". Sarebbe molto più difficile scrivere programmi se le lingue OO funzionassero diversamente. Ad esempio, funzioni e metodi non potrebbero mai aggiornare un oggetto. Java e la maggior parte dei linguaggi OO sarebbero quasi impossibili da usare senza una significativa complessità aggiunta.

Un oggetto in un programma dovrebbe avere un significato. Ad esempio rappresenta qualcosa di specifico nel mondo fisico reale. Di solito ha senso avere molti riferimenti alla stessa cosa. Ad esempio, il mio indirizzo di casa può essere dato a molte persone e organizzazioni e quell'indirizzo si riferisce sempre alla stessa posizione fisica. Quindi il primo punto è che gli oggetti spesso rappresentano qualcosa che è specifico, reale o concreto; e quindi essere in grado di avere molti riferimenti alla stessa cosa è estremamente utile. Altrimenti sarebbe più difficile scrivere programmi.

Ogni volta che si passa acome argomento / parametro a un'altra funzione, ad esempio chiamando
foo(Dog aDoggy);
o si applica un metodo a, il codice di programma sottostante crea una copia del riferimento, per produrre un secondo riferimento allo stesso oggetto.

Inoltre, se il codice con un riferimento copiato si trova in un thread diverso, entrambi possono essere utilizzati contemporaneamente per accedere allo stesso oggetto.

Quindi nella maggior parte dei programmi utili, ci saranno più riferimenti allo stesso oggetto, perché questa è la semantica della maggior parte dei linguaggi di programmazione OO.

Ora, se ci pensiamo, perché passare per riferimento è l' unico meccanismo disponibile in molti linguaggi OO (C ++ supporta entrambi), potremmo aspettarci che sia il comportamento predefinito "giusto" .

IMHO, l'utilizzo dei riferimenti è il valore predefinito predefinito , per un paio di motivi:

  1. Garantisce che il valore di un oggetto utilizzato in due luoghi diversi sia lo stesso. Immagina di inserire un oggetto in due diverse strutture di dati (array, elenchi ecc.) E di eseguire alcune operazioni su un oggetto che lo modifica. Potrebbe essere un incubo per il debug. Ancora più importante, è lo stesso oggetto in entrambe le strutture di dati o il programma ha un bug.
  2. È possibile convertire in modo felice il codice in più funzioni o unire il codice di più funzioni in una e la semantica non cambia. Se la lingua non fornisse la semantica di riferimento, sarebbe ancora più complesso modificare il codice.

C'è anche un argomento di efficienza; fare copie di interi oggetti è meno efficiente della copia di un riferimento. Tuttavia, penso che manchi il punto. I riferimenti multipli allo stesso oggetto hanno più senso e sono più facili da usare, poiché corrispondono alla semantica del mondo fisico reale.

Quindi, IMHO, di solito ha senso avere più riferimenti allo stesso oggetto. Nei casi insoliti in cui ciò non ha senso nel contesto di un algoritmo, la maggior parte delle lingue offre la possibilità di creare un "clone" o una copia approfondita. Tuttavia, questo non è il valore predefinito.

Penso che le persone che sostengono che questo non dovrebbe essere il default stiano usando un linguaggio che non fornisce la garbage collection automatica. Ad esempio, C ++ vecchio stile. Il problema è che devono trovare un modo per raccogliere oggetti "morti" e non recuperare oggetti che potrebbero essere ancora richiesti; avere più riferimenti allo stesso oggetto lo rende difficile.

Penso che se il C ++ avesse una garbage collection sufficientemente a basso costo, in modo che tutti gli oggetti referenziati fossero garbage collection, allora gran parte dell'obiezione scompare. Ci saranno ancora alcuni casi in cui la semantica di riferimento non è ciò che è necessario. Tuttavia, nella mia esperienza, le persone che sono in grado di identificare tali situazioni sono anche in genere in grado di scegliere comunque la semantica appropriata.

Credo che ci siano alcune prove che una grande quantità di codice in un programma C ++ è lì per gestire o mitigare la garbage collection. Tuttavia, scrivere e mantenere quel tipo di codice "infrastrutturale" aggiunge costi; è lì per rendere la lingua più facile da usare o più robusta. Quindi, ad esempio, il linguaggio Go è progettato con l'obiettivo di correggere alcuni dei punti deboli del C ++ e non ha altra scelta se non la garbage collection.

Ciò è ovviamente irrilevante nel contesto di Java. Anche questo è stato progettato per essere facile da usare, così come la raccolta dei rifiuti. Quindi avere più riferimenti è la semantica predefinita ed è relativamente sicura nel senso che gli oggetti non vengono recuperati mentre c'è un riferimento ad essi. Naturalmente potrebbero essere trattenuti da una struttura di dati perché il programma non riordina correttamente quando ha veramente finito con un oggetto.

Quindi, tornando indietro alla tua domanda (con un po 'di generalizzazione), quando vorresti più di un riferimento allo stesso oggetto? Praticamente in ogni situazione che mi viene in mente. Sono la semantica predefinita del meccanismo di passaggio dei parametri della maggior parte delle lingue. Suggerisco che ciò sia dovuto al fatto che la semantica predefinita della gestione degli oggetti che esiste nel mondo reale deve essere per riferimento (perché gli oggetti reali sono là fuori).

Qualsiasi altra semantica sarebbe più difficile da gestire.

Dog a = new Dog("rover");  // initialise with name 
DogList dl = new DogList()
dl.add(a)
...
a.setOwner("Mr Been")

Suggerisco che il "rover" in dldovrebbe essere quello effettuato setOwnero che i programmi diventino difficili da scrivere, comprendere, eseguire il debug o modificare. Penso che la maggior parte dei programmatori sarebbe perplessa o sconcertata altrimenti.

più tardi, il cane viene venduto:

soldDog = dl.lookupOwner("rover", "Mr Been")
soldDog.setOwner("Mr Mcgoo")

Questo tipo di elaborazione è comune e normale. Quindi la semantica di riferimento è l'impostazione predefinita perché di solito ha più senso.

Riepilogo: ha sempre senso avere più riferimenti allo stesso oggetto.


buon punto, ma questo è più adatto come commento
Bassinator

@HCBPshenanigans - Penso che il punto potrebbe essere stato troppo conciso e lasciato troppo non detto. Quindi ho ampliato il ragionamento. Penso che sia una risposta "meta" alla tua domanda. Riepilogo, più riferimenti allo stesso oggetto sono cruciali per rendere i programmi facili da scrivere perché molti oggetti in un programma rappresentano istanze specifiche o uniche delle cose nel mondo reale.
bagliore

1

Naturalmente, un altro scenario in cui potresti finire con:

Dog a = new Dog();
Dog b = a;

è quando si mantiene il codice e si butilizzava un cane diverso o una classe diversa, ma ora è gestito da a.

In genere, a medio termine, è necessario rielaborare tutto il codice per fare riferimento adirettamente, ma ciò potrebbe non accadere immediatamente.


1

Lo vorresti ogni volta che il tuo programma ha la possibilità di mettere in memoria un'entità in più di un punto, probabilmente perché diversi componenti lo stanno usando.

Identity Maps ha fornito un archivio locale definitivo di entità in modo da poter evitare di avere due o più rappresentazioni separate. Quando rappresentiamo lo stesso oggetto due volte, il nostro cliente corre il rischio di causare un problema di concorrenza se un riferimento all'oggetto persiste il suo stato cambia prima dell'altra istanza. L'idea è che vogliamo garantire che il nostro cliente tratti sempre il riferimento definitivo alla nostra entità / oggetto.


0

L'ho usato quando scrivevo un risolutore di Sudoku. Quando conosco il numero di una cella durante l'elaborazione delle righe, voglio che anche la colonna contenente conosca il numero di quella cella durante l'elaborazione delle colonne. Quindi sia le colonne che le righe sono matrici di oggetti Cell che si sovrappongono. Esattamente come mostrato dalla risposta accettata.


-2

Nelle applicazioni Web, gli Object Relational Mapper possono utilizzare il caricamento lento in modo che tutti i riferimenti allo stesso oggetto di database (almeno all'interno dello stesso thread) puntino alla stessa cosa.

Ad esempio, se avessi due tabelle:

Cani:

  • id | owner_id | nome
  • 1 | 1 | Bark Kent

Proprietari:

  • id | nome
  • 1 | me
  • 2 | voi

Esistono diversi approcci che l'ORM può eseguire se vengono effettuate le seguenti chiamate:

dog = Dog.find(1)  // fetch1
owner = Owner.find(1) // fetch2
superdog = owner.dogs.first() // fetch3
superdog.name = "Superdog"
superdog.save! // write1
owner = dog.owner // fetch4
owner.name = "Mark"
owner.save! // write2
dog.owner = Owner.find(2)
dog.save! // write3

Nella strategia ingenua, tutte le chiamate ai modelli e i relativi riferimenti recuperano oggetti separati. Dog.find(), Owner.find(), owner.dogs, E dog.ownerrisultato in una banca dati ha colpito la prima volta intorno, dopo di che vengono salvati nella memoria. E così:

  • il database viene recuperato da almeno 4 volte
  • dog.owner non è lo stesso di superdog.owner (recuperato separatamente)
  • dog.name non è lo stesso di superdog.name
  • dog e superdog tentano entrambi di scrivere nella stessa riga e si sovrascriveranno reciprocamente i risultati: write3 annullerà la modifica del nome in write1.

Senza riferimento, hai più recuperi, usi più memoria e introduci la possibilità di sovrascrivere gli aggiornamenti precedenti.

Supponiamo che il tuo ORM sappia che tutti i riferimenti alla riga 1 della tabella cani dovrebbero indicare la stessa cosa. Poi:

  • fetch4 può essere eliminato in quanto esiste un oggetto in memoria corrispondente a Owner.find (1). fetch3 comporterà comunque almeno una scansione dell'indice, poiché potrebbero esserci altri cani di proprietà del proprietario, ma non attiverà un recupero di riga.
  • cane e superdog puntano allo stesso oggetto.
  • dog.name e superdog.name puntano allo stesso oggetto.
  • dog.owner e superdog.owner puntano allo stesso oggetto.
  • write3 non sovrascrive la modifica in write1.

In altre parole, l'uso dei riferimenti aiuta a codificare il principio di un singolo punto di verità (almeno all'interno di quel thread) come riga nel database.

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.