Quand voudriez-vous deux références au même objet?

20

En Java en particulier, mais probablement dans d'autres langages également: quand serait-il utile d'avoir deux références au même objet?

Exemple:

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

Y a-t-il une situation où cela serait utile? Pourquoi serait-ce une solution préférée à utiliser achaque fois que vous souhaitez interagir avec l'objet représenté par a?

Bassinateur
la source
C'est l'essence même du motif Flyweight .
@MichaelT Pourriez-vous élaborer?
Bassinator
7
Si aet b toujours référence à la même chose Dog, cela ne sert à rien. S'ils le font parfois , c'est très utile.
Gabe

Réponses:

45

Un exemple est lorsque vous souhaitez avoir le même objet dans deux listes distinctes :

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

Une autre utilisation est lorsque vous avez le même objet jouant plusieurs rôles :

Person p = new Person("Bruce Wayne");
Person batman = p;
Person ceoOfWayneIndustries = p;
Tulains Córdova
la source
4
Je suis désolé mais je crois que pour votre exemple de Bruce Wayne, il y a un défaut de conception, le poste de batman et ceo devrait être un rôle pour votre personne.
Silviu Burcea
44
-1 pour avoir révélé l'identité secrète de Batman.
Ampt
2
@Silviu Burcea - Je suis fortement d'accord que l'exemple de Bruce Wayne n'est pas un bon exemple. D'une part, si une modification de texte globale a changé le nom `` ceoOfWayneIndustries '' et `` batman '' en `` p '' (en supposant qu'il n'y a pas de conflits de nom ou de changements de portée) et que la sémantique du programme a changé, alors c'est quelque chose de cassé. L'objet référencé représente la vraie signification, pas le nom de variable dans le programme. Pour avoir une sémantique différente, c'est soit un objet différent, soit référencé par quelque chose avec plus de comportement qu'une référence (qui devrait être transparente), et n'est donc pas un exemple de 2+ références.
gbulmer
2
Bien que l'exemple de Bruce Wayne tel qu'il soit écrit puisse ne pas fonctionner, je pense que l' intention déclarée est correcte. Un exemple plus proche pourrait être Persona batman = new Persona("Batman"); Persona bruce = new Persona("Bruce Wayne"); Persona currentPersona = batman;- où vous avez plusieurs valeurs possibles (ou une liste de valeurs disponibles) et une référence à celle actuellement active / sélectionnée.
Dan Puzey
1
@ bulbul: Je suppose que vous ne pouvez pas avoir de référence à deux objets; la référence currentPersonapointe vers un objet ou l'autre, mais jamais les deux. En particulier, il peut facilement être possible que currentPersonaest jamais réglé sur bruce, auquel cas il est certainement pas une référence à deux objets. Je dirais que, dans mon exemple, les deux batmanet currentPersonasont des références à la même instance, mais servent une signification sémantique différente dans le programme.
Dan Puzey
16

C'est en fait une question étonnamment profonde! L'expérience du C ++ moderne (et des langages issus du C ++ moderne, comme Rust) suggère que très souvent, vous ne voulez pas ça! Pour la plupart des données, vous voulez une référence unique ou unique ("propriétaire"). Cette idée est également la raison principale des systèmes de type linéaire .

Cependant, même dans ce cas, vous voulez généralement des références "empruntées" de courte durée qui sont utilisées pour accéder brièvement à la mémoire, mais qui ne durent pas pendant une fraction significative du temps où les données existent. Le plus souvent, lorsque vous passez un objet comme argument à une fonction différente (les paramètres sont aussi des variables!):

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

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

Un cas moins courant lorsque vous utilisez l'un des deux objets en fonction d'une condition, faisant essentiellement la même chose quel que soit votre choix:

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

Pour revenir à des utilisations plus courantes, mais en laissant les variables locales derrière, nous nous tournons vers les champs: par exemple, les widgets dans les frameworks GUI ont souvent des widgets parents, donc votre grand cadre contenant dix boutons aurait au moins dix références pointant dessus (plus quelques autres de son parent et peut-être des auditeurs d'événements, etc.). Tout type de graphe d'objets, et certains types d'arbres d'objets (ceux avec des références parent / frère), ont plusieurs objets se référant chacun au même objet. Et pratiquement chaque ensemble de données est en fait un graphique ;-)


la source
3
Le «cas le moins courant» est assez courant: vous avez les objets stockés dans une liste ou une carte, récupérez celui que vous voulez et effectuez les opérations requises. Vous n'effacez pas leurs références de la carte ou de la liste.
SJuan76
1
@ SJuan76 Le "cas le moins courant" concerne uniquement la prise de variables locales, tout ce qui implique des structures de données relève du dernier point.
Cet idéal d'une seule référence est-il dû à la gestion de la mémoire de comptage des références? Si c'est le cas, il convient de mentionner que la motivation n'est pas pertinente pour d'autres langages avec différents garbage collector (par exemple C #, Java, Python)
MarkJ
Les types @MarkJ Linear ne sont pas seulement un idéal, beaucoup de code existant s'y conforme inconsciemment parce que c'est la chose sémantiquement correcte pour un bon nombre de cas. Il présente des avantages potentiels en termes de performances (mais ni pour le comptage des références ni pour le suivi des GC, il n'aide que lorsque vous utilisez une stratégie différente qui exploite réellement ces connaissances pour omettre à la fois les refcounts et le traçage). Plus intéressante est son application à une gestion simple et déterministe des ressources. Pensez que RAII et C ++ 11 déplacent la sémantique, mais mieux (s'applique plus souvent et les erreurs sont détectées par le compilateur).
6

Variables temporaires: considérez le pseudocode suivant.

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

Les variables maxet candidatepeuvent pointer vers le même objet, mais l'affectation des variables change en utilisant des règles différentes et à des moments différents.


la source
3

Pour compléter les autres réponses, vous pouvez également vouloir parcourir une structure de données différemment, en partant du même endroit. Par exemple, si vous aviez un BinaryTree a = new BinaryTree(...); BinaryTree b = a, vous pourriez parcourir le chemin le plus à gauche de l'arbre avec aet son chemin le plus à droite avec b, en utilisant quelque chose comme:

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

Cela fait un moment que je n'ai pas écrit Java, de sorte que le code peut ne pas être correct ou sensé. Prenez-le plus comme pseudocode.

Mike
la source
3

Cette méthode est idéale lorsque vous avez plusieurs objets qui rappellent tous un autre objet qui peut être utilisé de manière non contextuelle.

Par exemple, si vous avez une interface à onglets, vous pouvez avoir Tab1, Tab2 et Tab3. Vous voudrez peut-être également pouvoir utiliser une variable commune quel que soit l'onglet sur lequel l'utilisateur se trouve pour simplifier votre code et éviter d'avoir à comprendre à la volée et sur quel onglet votre utilisateur se trouve.

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

Ensuite, dans chacun des onglets numérotés onClick, vous pouvez modifier CurrentTab pour référencer cet onglet.

CurrentTab = Tab3;

Maintenant, dans votre code, vous pouvez appeler "CurrentTab" en toute impunité sans avoir besoin de savoir sur quel onglet vous vous trouvez. Vous pouvez également mettre à jour les propriétés de CurrentTab et elles descendent automatiquement vers l'onglet référencé.

JMD
la source
3

Il existe de nombreux scénarios qui b doivent être une référence à un " a" inconnu pour être utiles. En particulier:

  • Chaque fois que vous ne savez pas ce qui bpointe au moment de la compilation.
  • Chaque fois que vous devez parcourir une collection, qu'elle soit connue au moment de la compilation ou non
  • Chaque fois que vous avez une portée limitée

Par exemple:

Paramètres

public void DoSomething(Thing &t) {
}

t est une référence à une variable d'une portée extérieure.

Valeurs de retour et autres valeurs conditionnelles

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

biggerThinget zsont chacun des références à aou b. Nous ne savons pas lequel au moment de la compilation.

Lambdas et leurs valeurs de retour

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

xest un paramètre (exemple 1 ci-dessus), et test une valeur de retour (exemple 2 ci-dessus)

Les collections

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

index["some name"]est, dans une large mesure, une apparence plus sophistiquée b. C'est un alias pour un objet qui a été créé et inséré dans la collection.

Boucles

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

t est une référence à un élément retourné (exemple 2) par un itérateur (exemple précédent).

svidgen
la source
Vos exemples sont difficiles à suivre. Ne vous méprenez pas, je les ai surmontés, mais je devais y travailler. À l'avenir, je suggère de séparer vos exemples de code en blocs individuels (avec quelques notes expliquant spécifiquement chacun), sans mettre quelques exemples sans rapport dans un bloc.
Bassinator
1
@HCBPshenanigans Je ne m'attends pas à ce que vous changiez les réponses sélectionnées; mais j'ai mis à jour le mien pour, espérons-le, aider à la lisibilité et remplir certains des cas d'utilisation manquants dans la réponse sélectionnée.
svidgen
2

C'est un point crucial, mais à mon humble avis, cela vaut la peine d'être compris.

Toutes les langues OO font toujours des copies de références, et ne copient jamais un objet «de manière invisible». Il serait beaucoup plus difficile d'écrire des programmes si les langues OO fonctionnaient d'une autre manière. Par exemple, les fonctions et les méthodes n'ont jamais pu mettre à jour un objet. Java et la plupart des langages OO seraient presque impossibles à utiliser sans une complexité supplémentaire importante.

Un objet dans un programme est censé avoir une signification. Par exemple, il représente quelque chose de spécifique dans le monde physique réel. Il est généralement logique d'avoir de nombreuses références à la même chose. Par exemple, mon adresse personnelle peut être communiquée à de nombreuses personnes et organisations, et cette adresse fait toujours référence au même emplacement physique. Donc, le premier point est que les objets représentent souvent quelque chose de spécifique, réel ou concret; et donc être capable d'avoir de nombreuses références à la même chose est extrêmement utile. Sinon, il serait plus difficile d'écrire des programmes.

Chaque fois que tu passes a comme argument / paramètre à une autre fonction, par exemple en appelant
foo(Dog aDoggy);
ou en appliquant une méthode à a, le code de programme sous-jacent fait une copie de la référence, pour produire une deuxième référence au même objet.

De plus, si le code avec une référence copiée se trouve dans un thread différent, les deux peuvent être utilisés simultanément pour accéder au même objet.

Ainsi, dans la plupart des programmes utiles, il y aura plusieurs références au même objet, car c'est la sémantique de la plupart des langages de programmation OO.

Maintenant, si nous y réfléchissons, car le passage par référence est le seul mécanisme disponible dans de nombreux langages OO (C ++ prend en charge les deux), nous pourrions nous attendre à ce qu'il soit le `` bon '' comportement par défaut .

À mon humble avis, l'utilisation de références est la bonne valeur par défaut , pour deux raisons:

  1. Il garantit que la valeur d'un objet utilisé à deux endroits différents est la même. Imaginez mettre un objet dans deux structures de données différentes (tableaux, listes, etc.) et effectuer certaines opérations sur un objet qui le modifie. Cela pourrait être un cauchemar à déboguer. Plus important encore, il est le même objet dans les deux structures de données, ou le programme a un bug.
  2. Vous pouvez joyeusement refactoriser le code en plusieurs fonctions, ou fusionner le code de plusieurs fonctions en une seule, et la sémantique ne change pas. Si le langage ne fournissait pas de sémantique de référence, il serait encore plus complexe de modifier le code.

Il y a aussi un argument d'efficacité; la copie d'objets entiers est moins efficace que la copie d'une référence. Cependant, je pense que cela manque le point. Les références multiples au même objet ont plus de sens et sont plus faciles à utiliser, car elles correspondent à la sémantique du monde physique réel.

Donc, à mon humble avis, il est généralement logique d'avoir plusieurs références au même objet. Dans les cas inhabituels où cela n'a pas de sens dans le contexte d'un algorithme, la plupart des langues offrent la possibilité de faire un «clone» ou une copie complète. Cependant, ce n'est pas la valeur par défaut.

Je pense que les gens qui soutiennent que cela ne devrait pas être la valeur par défaut utilisent un langage qui ne fournit pas de collecte automatique des ordures. Par exemple, le C ++ à l'ancienne. Le problème est qu'ils doivent trouver un moyen de collecter des objets «morts» et non de récupérer des objets qui pourraient encore être nécessaires; avoir plusieurs références au même objet rend cela difficile.

Je pense que si C ++ avait une collecte des ordures suffisamment peu coûteuse, de sorte que tous les objets référencés soient récupérés, alors une grande partie de l'objection disparaît. Il y aura toujours des cas où la sémantique de référence n'est pas ce qui est nécessaire. Cependant, d'après mon expérience, les personnes qui peuvent identifier ces situations sont également généralement capables de choisir la sémantique appropriée de toute façon.

Je crois qu'il existe des preuves qu'une grande quantité de code dans un programme C ++ est là pour gérer ou atténuer la collecte des ordures. Cependant, l'écriture et le maintien de ce type de code «infrastructurel» augmentent les coûts; il est là pour rendre le langage plus facile à utiliser ou plus robuste. Ainsi, par exemple, le langage Go est conçu en mettant l'accent sur la correction de certaines des faiblesses de C ++, et il n'a pas d'autre choix que la récupération de place.

Ceci n'est bien sûr pas pertinent dans le contexte de Java. Il a également été conçu pour être facile à utiliser, tout comme la collecte des ordures. Par conséquent, avoir plusieurs références est la sémantique par défaut et est relativement sûr dans le sens où les objets ne sont pas récupérés tant qu'il y a une référence à eux. Bien sûr, ils peuvent être conservés par une structure de données car le programme ne se range pas correctement lorsqu'il a vraiment fini avec un objet.

Donc, en revenant à votre question (avec un peu de généralisation), quand voudriez-vous plus d'une référence au même objet? À peu près dans toutes les situations auxquelles je peux penser. Ils sont la sémantique par défaut du mécanisme de passage de paramètres de la plupart des langues. Je suggère que c'est parce que la sémantique par défaut de la manipulation des objets qui existe dans le monde réel doit à peu près être par référence ('car les objets réels sont là-bas).

Toute autre sémantique serait plus difficile à gérer.

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

Je suggère que le "rover" dans dlsoit celui effectué par setOwnerou les programmes deviennent difficiles à écrire, à comprendre, à déboguer ou à modifier. Je pense que la plupart des programmeurs seraient perplexes ou consternés autrement.

plus tard, le chien est vendu:

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

Ce type de traitement est courant et normal. La sémantique de référence est donc la valeur par défaut, car elle est généralement plus logique.

Résumé: Il est toujours judicieux d'avoir plusieurs références au même objet.

gbulmer
la source
bon point, mais cela convient mieux comme commentaire
Bassinator
@HCBPshenanigans - Je pense que le point aurait pu être trop laconique et laisser trop de choses non dites. J'ai donc développé le raisonnement. Je pense que c'est une «méta» réponse à votre question. Résumé, plusieurs références au même objet sont cruciales pour rendre les programmes faciles à écrire car de nombreux objets dans un programme représentent des instances spécifiques ou uniques de choses dans le monde réel.
gbulmer
1

Bien sûr, un autre scénario où vous pourriez vous retrouver avec:

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

c'est lorsque vous maintenez le code et que vous étiez bautrefois un chien ou une classe différente, mais qu'il est désormais desservi par a.

En règle générale, à moyen terme, vous devez retravailler tout le code pour vous y référer adirectement, mais cela peut ne pas se produire immédiatement.

Mark Hurd
la source
1

Vous voudriez cela à chaque fois que votre programme a une chance de tirer une entité en mémoire à plusieurs endroits, probablement parce que différents composants l'utilisent.

Les cartes d'identité ont fourni un magasin local définitif d'entités afin que nous puissions éviter d'avoir deux ou plusieurs représentations distinctes. Lorsque nous représentons le même objet deux fois, notre client court le risque de provoquer un problème de concurrence si une référence de l'objet persiste dans ses changements d'état avant l'autre instance. L'idée est que nous voulons nous assurer que notre client traite toujours la référence définitive à notre entité / objet.

Mario T. Lanza
la source
0

Je l'ai utilisé lors de l'écriture d'un solveur Sudoku. Lorsque je connais le numéro d'une cellule lors du traitement des lignes, je veux que la colonne contenant connaisse également le numéro de cette cellule lors du traitement des colonnes. Les colonnes et les lignes sont donc des tableaux d'objets Cell qui se chevauchent. Exactement comme l'a montré la réponse acceptée.

accolade
la source
-2

Dans les applications Web, les mappeurs relationnels d'objets peuvent utiliser le chargement différé de sorte que toutes les références au même objet de base de données (au moins dans le même thread) pointent vers la même chose.

Par exemple, si vous aviez deux tables:

Chiens:

  • id | owner_id | Nom
  • 1 | 1 | Bark Kent

Propriétaires:

  • id | Nom
  • 1 | moi
  • 2 | vous

Votre ORM peut effectuer plusieurs approches si les appels suivants sont effectués:

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

Dans la stratégie naïve, tous les appels aux modèles et aux références associées récupèrent des objets séparés. Dog.find(), Owner.find(), owner.dogsEt dog.ownerrésultat dans une base de données a frappé la première fois, après quoi ils sont enregistrés dans la mémoire. Et donc:

  • la base de données est extraite d'au moins 4 fois
  • dog.owner n'est pas le même que superdog.owner (récupéré séparément)
  • dog.name n'est pas la même chose que superdog.name
  • dog et superdog tentent tous les deux d'écrire sur la même ligne et se remplacent mutuellement les résultats: write3 annulera le changement de nom dans write1.

Sans référence, vous avez plus de récupérations, utilisez plus de mémoire et introduisez la possibilité d'écraser les mises à jour antérieures.

Supposons que votre ORM sache que toutes les références à la ligne 1 du tableau des chiens doivent pointer vers la même chose. Alors:

  • fetch4 peut être éliminé car il y a un objet en mémoire correspondant à Owner.find (1). fetch3 entraînera toujours au moins une analyse d'index, car il peut y avoir d'autres chiens appartenant au propriétaire, mais ne déclenchera pas de récupération de ligne.
  • chien et super-chien pointent vers le même objet.
  • dog.name et superdog.name pointent vers le même objet.
  • dog.owner et superdog.owner pointent vers le même objet.
  • write3 n'écrase pas la modification de write1.

En d'autres termes, l'utilisation de références aide à codifier le principe d'un seul point de vérité (au moins à l'intérieur de ce fil) étant la ligne de la base de données.

madumlao
la source