Kiedy chcesz dwa odniesienia do tego samego obiektu?


20

Szczególnie w Javie, ale prawdopodobnie także w innych językach: kiedy przydatne byłyby dwa odniesienia do tego samego obiektu?

Przykład:

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

Czy istnieje sytuacja, w której byłoby to przydatne? Dlaczego miałoby to być preferowanym rozwiązaniem, ajeśli chcesz korzystać z obiektu reprezentowanego przez a?


To jest esencja wzoru Flyweight .

@MichaelT Mógłbyś opracować?
Bassinator

7
Jeśli ai b zawsze odnoszą się do tego samego Dog, to nie ma sensu. Jeśli czasem mogą, to jest to całkiem przydatne.
Gabe,

Odpowiedzi:


45

Przykładem może być ten sam obiekt na dwóch osobnych listach :

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

Innym zastosowaniem jest użycie tego samego obiektu w kilku rolach :

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

4
Przykro mi, ale wierzę, że w twoim przykładzie Bruce'a Wayne'a jest wada projektowa, batman i pozycja prezesa powinny być rolami dla twojej osoby.
Silviu Burcea

44
-1 za ujawnienie tajnej tożsamości Batmana.
Ampt

2
@Silviu Burcea - zdecydowanie zgadzam się, że przykład Bruce'a Wayne'a nie jest dobrym przykładem. Z jednej strony, jeśli globalna edycja tekstu zmieniła nazwy „ceoOfWayneIndustries” i „batman” na „p” (przy założeniu braku kolizji nazwy lub zmiany zakresu), a semantyka programu uległa zmianie, to ich coś jest zepsute. Odwołany obiekt reprezentuje prawdziwe znaczenie, a nie nazwę zmiennej w programie. Aby mieć inną semantykę, jest to albo inny obiekt, albo odwołuje się do niego coś, co ma więcej zachowań niż odwołanie (które powinno być przezroczyste), a więc nie jest to przykład ponad 2 odnośników.
żaglowiec

2
Chociaż przykład Bruce'a Wayne'a w formie pisemnej może nie działać, uważam, że podana intencja jest poprawna. Być może bliższym przykładem może być Persona batman = new Persona("Batman"); Persona bruce = new Persona("Bruce Wayne"); Persona currentPersona = batman;- gdzie masz wiele możliwych wartości (lub listę dostępnych wartości) i odniesienie do aktualnie aktywnej / wybranej.
Dan Puzey,

1
@gbulmer: Zakładam, że nie możesz mieć odniesienia do dwóch obiektów; odniesienie currentPersonawskazuje na jeden przedmiot lub drugi, ale nigdy oba. W szczególności może być łatwo możliwe, że nigdy niecurrentPersona jest ustawione , w takim przypadku z pewnością nie jest to odniesienie do dwóch obiektów. Powiedziałbym, że w moim przykładzie, zarówno i są odniesienia do tej samej instancji, lecz służą różne znaczenie semantyczne w ramach programu. brucebatmancurrentPersona
Dan Puzey,

16

To właściwie zaskakująco głębokie pytanie! Doświadczenie ze współczesnego C ++ (i języków zaczerpniętych ze współczesnego C ++, takich jak Rust) sugeruje, że bardzo często tego nie chcesz! W przypadku większości danych potrzebujesz pojedynczego lub unikalnego odwołania („będącego właścicielem”). Pomysł ten jest również jednym z głównych powodów stosowania systemów liniowych .

Jednak nawet wtedy zwykle potrzebujesz krótkich „pożyczonych” referencji, które są używane do szybkiego dostępu do pamięci, ale nie trwają przez znaczną część czasu, w którym dane istnieją. Najczęściej, gdy przekazujesz obiekt jako argument do innej funkcji (parametry też są zmienne!):

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

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

Rzadszy przypadek, gdy używasz jednego z dwóch obiektów w zależności od warunku, robiąc zasadniczo to samo, niezależnie od tego, który wybierzesz:

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

Wracając do bardziej powszechnych zastosowań, ale pozostawiając lokalne zmienne, zwracamy się do pól: Na przykład widżety w ramach GUI często mają widżety nadrzędne, więc twoja duża ramka zawierająca dziesięć przycisków miałaby co najmniej dziesięć odnośników wskazujących na nią (plus kilka innych od jego rodzica i być może od słuchaczy wydarzeń i tak dalej). Każdy rodzaj wykresu obiektowego i niektóre rodzaje drzew obiektowych (te z odniesieniami do rodzica / rodzeństwa) mają wiele obiektów odnoszących się do każdego tego samego obiektu. I praktycznie każdy zestaw danych to tak naprawdę wykres ;-)


3
„Rzadszy przypadek” jest dość powszechny: obiekty są przechowywane na liście lub mapie, pobierz ten, który chcesz, i wykonaj wymagane operacje. Nie usuwasz ich odniesień z mapy ani listy.
SJuan76,

1
@ SJuan76 „Mniej powszechny przypadek” polega wyłącznie na pobieraniu zmiennych lokalnych, wszystko, co dotyczy struktur danych, należy do ostatniego punktu.

Czy to ideał jest tylko jednym odniesieniem ze względu na zarządzanie pamięcią liczenia odniesień? Jeśli tak, warto wspomnieć, że motywacja nie dotyczy innych języków z różnymi śmieciarzami (np. C #, Java, Python)
MarkJ

@ MarkJ Typy liniowe to nie tylko idealny, wiele istniejących kodów nieświadomie się z nim zgadza, ponieważ jest to poprawność semantyczna w wielu przypadkach. Ma to potencjalne zalety pod względem wydajności (ale ani do zliczania referencji, ani śledzenia GC, pomaga tylko wtedy, gdy używasz innej strategii, która faktycznie wykorzystuje tę wiedzę, aby pominąć zarówno przeliczanie rachunków, jak i śledzenie). Bardziej interesująca jest jej aplikacja do prostego, deterministycznego zarządzania zasobami. Pomyśl, że RAII i C ++ 11 przenoszą semantykę, ale lepiej (stosuje się częściej, a kompilator przechwytuje błędy).

6

Zmienne tymczasowe: rozważ następujący pseudokod.

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

Zmienne maxi candidatemogą wskazywać na ten sam obiekt, ale przypisanie zmiennych zmienia się przy użyciu różnych reguł i w różnym czasie.


3

Aby uzupełnić inne odpowiedzi, możesz również przejrzeć strukturę danych inaczej, zaczynając od tego samego miejsca. Na przykład, jeśli masz BinaryTree a = new BinaryTree(...); BinaryTree b = a, możesz przejść w dół do lewej ścieżki drzewa ai jej prawej ścieżki b, używając czegoś takiego:

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

Minęło trochę czasu, odkąd napisałem Javę, więc kod może być niepoprawny lub rozsądny. Weź to bardziej jako pseudokod.


3

Ta metoda jest świetna, gdy masz kilka obiektów, które wszystkie oddzwaniają do innego obiektu, z którego można korzystać niekoniecznie.

Na przykład, jeśli masz interfejs z kartami, możesz mieć Tab1, Tab2 i Tab3. Możesz także chcieć używać wspólnej zmiennej, niezależnie od tego, na której karcie użytkownik jest włączony, aby uprościć kod i zmniejszyć konieczność ciągłego zastanawiania się nad tym, na której karcie znajduje się użytkownik.

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

Następnie na każdej z ponumerowanych kart naClick można zmienić CurrentTab, aby odwoływać się do tej karty.

CurrentTab = Tab3;

Teraz w swoim kodzie możesz bezkarnie wywoływać „CurrentTab”, nie musząc wiedzieć, na której karcie się aktualnie znajdujesz. Możesz także zaktualizować właściwości CurrentTab, a one automatycznie spłyną do odnośnej karty.


3

Istnieje wiele scenariuszy, w których b musi być odniesienie do nieznanego „ a”, aby było przydatne. W szczególności:

  • Za każdym razem, gdy nie wiesz, co bwskazuje na czas kompilacji.
  • Za każdym razem, gdy trzeba iterować kolekcję, niezależnie od tego, czy jest ona znana w czasie kompilacji, czy nie
  • Za każdym razem, gdy masz ograniczony zakres

Na przykład:

Parametry

public void DoSomething(Thing &t) {
}

t jest odniesieniem do zmiennej z zakresu zewnętrznego.

Zwracane wartości i inne wartości warunkowe

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

biggerThingi zkażdy z nich jest odniesieniem do jednego alub b. Nie wiemy, które w czasie kompilacji.

Lambdas i ich wartości zwracane

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

xjest parametrem (przykład 1 powyżej) i tjest wartością zwracaną (przykład 2 powyżej)

Kolekcje

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

index["some name"]jest w dużej mierze bardziej wyrafinowany b. Jest to alias do obiektu, który został utworzony i umieszczony w kolekcji.

Pętle

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

t jest odwołaniem do elementu zwróconego (przykład 2) przez iterator (poprzedni przykład).


Twoje przykłady są trudne do naśladowania. Nie zrozum mnie źle, przeszedłem przez nie, ale musiałem nad tym popracować. W przyszłości sugeruję rozdzielenie przykładów kodu na poszczególne bloki (z pewnymi uwagami wyjaśniającymi każdy z nich), a nie umieszczanie kilku niepowiązanych przykładów w jednym bloku.
Bassinator

1
@HCBPshenanigans Nie oczekuję, że zmienisz wybrane odpowiedzi; ale zaktualizowałem mój, aby, mam nadzieję, poprawić czytelność i uzupełnić niektóre przypadki użycia, których brakuje w wybranej odpowiedzi.
svidgen

2

Jest to kluczowa kwestia, ale IMHO jest warte zrozumienia.

Wszystkie języki OO zawsze wykonują kopie referencji i nigdy nie kopiują obiektu „niewidocznie”. O wiele trudniej byłoby pisać programy, gdyby języki OO działały w jakikolwiek inny sposób. Na przykład funkcje i metody nigdy nie mogłyby zaktualizować obiektu. Java i większość języków OO byłyby prawie niemożliwe do użycia bez znacznej dodatkowej złożoności.

Obiekt w programie powinien mieć jakieś znaczenie. Na przykład reprezentuje coś konkretnego w prawdziwym świecie fizycznym. Zwykle wiele odniesień do tej samej rzeczy ma sens. Na przykład mój adres domowy można podać wielu osobom i organizacjom, a adres ten zawsze odnosi się do tej samej fizycznej lokalizacji. Pierwszą kwestią jest to, że obiekty często przedstawiają coś konkretnego, rzeczywistego lub konkretnego; dlatego posiadanie wielu odniesień do tej samej rzeczy jest niezwykle przydatne. W przeciwnym razie trudniej byłoby pisać programy.

Za każdym razem, agdy przekazujesz jako argument / parametr do innej funkcji, np. Wywołania
foo(Dog aDoggy);
lub zastosowania metody a, bazowy kod programu tworzy kopię odwołania, aby utworzyć drugie odwołanie do tego samego obiektu.

Ponadto, jeśli kod z skopiowanym odwołaniem znajduje się w innym wątku, oba mogą być używane jednocześnie, aby uzyskać dostęp do tego samego obiektu.

Tak więc w najbardziej przydatnych programach będzie wiele odniesień do tego samego obiektu, ponieważ jest to semantyka większości języków programowania OO.

Teraz, jeśli się nad tym zastanowimy, ponieważ przekazywanie przez referencje jest jedynym mechanizmem dostępnym w wielu językach OO (C ++ obsługuje oba), możemy oczekiwać, że będzie to „prawidłowe” zachowanie domyślne .

IMHO, używanie referencji jest właściwym ustawieniem domyślnym z kilku powodów:

  1. Gwarantuje to, że wartość obiektu użytego w dwóch różnych miejscach jest taka sama. Wyobraź sobie umieszczenie obiektu w dwóch różnych strukturach danych (tablice, listy itp.) I wykonanie pewnych operacji na obiekcie, który go zmienia. Debugowanie może być koszmarem. Co ważniejsze, jest to ten sam obiekt w obu strukturach danych lub w programie występuje błąd.
  2. Możesz szczęśliwie zmienić kod na kilka funkcji lub scalić kod z kilku funkcji w jedną, a semantyka się nie zmienia. Gdyby język nie zapewniał semantyki odniesienia, modyfikacja kodu byłaby jeszcze bardziej złożona.

Istnieje również argument dotyczący wydajności; wykonywanie kopii całych obiektów jest mniej wydajne niż kopiowanie odwołania. Myślę jednak, że to nie ma sensu. Liczne odniesienia do tego samego obiektu mają większy sens i są łatwiejsze w użyciu, ponieważ pasują do semantyki prawdziwego świata fizycznego.

Tak więc, IMHO, zwykle ma sens wiele odniesień do tego samego obiektu. W nietypowych przypadkach, gdy nie ma to sensu w kontekście algorytmu, większość języków zapewnia możliwość „klonowania” lub głębokiej kopii. Nie jest to jednak domyślne.

Myślę, że ludzie, którzy twierdzą, że to nie powinno być domyślne, używają języka, który nie zapewnia automatycznego usuwania śmieci. Na przykład staroświecki C ++. Problem polega na tym, że muszą znaleźć sposób na zbieranie „martwych” przedmiotów, a nie na odzyskiwanie przedmiotów, które mogą być nadal potrzebne; posiadanie wielu odniesień do tego samego obiektu sprawia, że ​​jest to trudne.

Myślę, że jeśli C ++ miał wystarczająco tanie wyrzucanie elementów bezużytecznych, aby wszystkie obiekty, do których istnieją odniesienia, były usuwane, to znaczna część sprzeciwu zniknęła. Nadal będą przypadki, w których semantyka odniesienia nie jest potrzebna. Jednak z mojego doświadczenia wynika, że ​​ludzie, którzy potrafią zidentyfikować te sytuacje, zazwyczaj są w stanie wybrać odpowiednią semantykę.

Uważam, że istnieją pewne dowody na to, że duża część kodu w programie C ++ służy do obsługi lub łagodzenia usuwania śmieci. Jednak pisanie i utrzymywanie tego rodzaju „infrastrukturalnego” kodu zwiększa koszty; ma na celu ułatwienie korzystania z języka lub zwiększenie jego niezawodności. Na przykład język Go został zaprojektowany z naciskiem na usunięcie niektórych słabości C ++ i nie ma innego wyboru niż wyrzucanie elementów bezużytecznych.

Jest to oczywiście nieistotne w kontekście Java. To również zostało zaprojektowane tak, aby było łatwe w użyciu, podobnie jak zbieranie śmieci. Dlatego posiadanie wielu odniesień jest domyślną semantyką i jest względnie bezpieczne w tym sensie, że obiekty nie są odzyskiwane, gdy istnieje odniesienie do nich. Oczywiście mogą być trzymane przez strukturę danych, ponieważ program nie porządkuje właściwie, gdy naprawdę zakończy się na obiekcie.

Powracając do pytania (z pewnym uogólnieniem), kiedy chciałbyś mieć więcej niż jedno odniesienie do tego samego obiektu? Prawie w każdej sytuacji, o której mogę pomyśleć. Są one domyślną semantyką większości mechanizmów przekazywania parametrów języków. Sugeruję, że dzieje się tak, ponieważ domyślna semantyka obsługi obiektów, która istnieje w realnym świecie, musi być w zasadzie przez odniesienie (ponieważ rzeczywiste obiekty są tam).

Każda inna semantyka byłaby trudniejsza do opanowania.

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

Sugeruję, że „łazik” dlpowinien być tym, na który wpływ mają setOwnerlub programy będą miały trudności z pisaniem, zrozumieniem, debugowaniem lub modyfikacją. Myślę, że większość programistów byłaby zdziwiona lub przerażona inaczej.

później pies jest sprzedawany:

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

Ten rodzaj przetwarzania jest powszechny i ​​normalny. Tak więc semantyka odniesienia jest domyślna, ponieważ zazwyczaj ma ona największy sens.

Podsumowanie: Zawsze warto mieć wiele odwołań do tego samego obiektu.


dobra uwaga, ale lepiej to komentować
Bassinator

@HCBPshenanigans - Wydaje mi się, że punkt ten był zbyt zwięzły i pozostawił zbyt wiele do powiedzenia. Więc rozwinąłem rozumowanie. Myślę, że jest to „meta” odpowiedź na twoje pytanie. Podsumowanie, wiele odniesień do tego samego obiektu ma kluczowe znaczenie dla ułatwienia pisania programów, ponieważ wiele obiektów w programie reprezentuje określone lub unikalne przypadki rzeczy w świecie rzeczywistym.
żaglowiec

1

Oczywiście jeszcze jeden scenariusz, w którym możesz skończyć z:

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

ma miejsce, gdy utrzymujesz kod i bkiedyś byłeś innym psem lub inną klasą, ale teraz jest obsługiwany przez a.

Ogólnie rzecz biorąc, w średnim okresie powinieneś przerobić cały kod, aby odwoływał się abezpośrednio, ale może się to nie zdarzyć od razu.


1

Chcesz tego w dowolnym momencie, gdy Twój program ma szansę wciągnąć byt do pamięci w więcej niż jednym miejscu, być może dlatego, że używają go różne komponenty.

Mapy tożsamości stanowiły ostateczny lokalny magazyn podmiotów, dzięki czemu możemy uniknąć posiadania dwóch lub więcej osobnych reprezentacji. Kiedy reprezentujemy ten sam obiekt dwa razy, nasz klient ryzykuje spowodowanie problemu z współbieżnością, jeśli jedno odwołanie do obiektu będzie się zmieniać jego stan, zanim zrobi to druga instancja. Chodzi o to, aby zapewnić, że nasz klient zawsze ma ostateczne odniesienie do naszego bytu / obiektu.


0

Użyłem tego, pisząc solver Sudoku. Kiedy znam numer komórki podczas przetwarzania wierszy, chcę, aby zawierająca kolumna znała również numer tej komórki podczas przetwarzania kolumn. Tak więc zarówno kolumny, jak i wiersze są tablicami obiektów Cell, które się nakładają. Dokładnie tak, jak pokazała zaakceptowana odpowiedź.


-2

W aplikacjach webowych Object Relational Mappers może korzystać z leniwego ładowania, aby wszystkie odwołania do tego samego obiektu bazy danych (przynajmniej w tym samym wątku) wskazywały na to samo.

Na przykład, jeśli masz dwie tabele:

Psy:

  • id | ID_użytkownika | Nazwa
  • 1 | 1 | Bark Kent

Właściciele:

  • id | Nazwa
  • 1 | mnie
  • 2 | ty

Istnieje kilka metod, które może wykonać ORM, jeśli wykonane zostaną następujące połączenia:

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

W naiwnej strategii wszystkie wywołania modeli i powiązane odniesienia pobierają oddzielne obiekty. Dog.find(), Owner.find(), owner.dogs, A dog.ownerwynik w bazie danych trafić za pierwszym razem, po czym są one zapisywane w pamięci. A więc:

  • baza danych jest pobierana co najmniej 4 razy
  • dog.owner to nie to samo co superdog.owner (pobierane osobno)
  • dog.name nie jest tym samym co superdog.name
  • pies i superdog próbują pisać w tym samym wierszu i wzajemnie nadpisują wyniki: write3 cofnie zmianę nazwy w write1.

Bez odniesienia masz więcej pobrań, więcej pamięci i wprowadzasz możliwość nadpisywania wcześniejszych aktualizacji.

Załóżmy, że Twoja ORM wie, że wszystkie odniesienia do wiersza 1 tabeli psów powinny wskazywać na to samo. Następnie:

  • fetch4 można wyeliminować, ponieważ w pamięci znajduje się obiekt odpowiadający Owner.find (1). fetch3 nadal spowoduje przynajmniej skanowanie indeksu, ponieważ właściciel może mieć inne psy, ale nie uruchomi wyszukiwania wiersza.
  • pies i superdog wskazują na ten sam obiekt.
  • dog.name i superdog.name wskazują na ten sam obiekt.
  • dog.owner i superdog.owner wskazują na ten sam obiekt.
  • write3 nie zastępuje zmiany w write1.

Innymi słowy, użycie referencji pomaga skodyfikować zasadę, że jednym punktem prawdy (przynajmniej w tym wątku) jest wiersz w bazie danych.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.