Młody współpracownik, który studiował OO, zapytał mnie, dlaczego każdy przedmiot jest przekazywany przez referencję, co jest przeciwieństwem prymitywnych typów lub struktur. Jest to wspólna cecha języków takich jak Java i C #.
Nie mogłem znaleźć dla niego dobrej odpowiedzi.
Jakie są motywy tej decyzji projektowej? Czy programiści tych języków byli zmęczeni tworzeniem wskaźników za każdym razem?
programming-languages
object-oriented
Gustavo Cardoso
źródło
źródło
Odpowiedzi:
Podstawowe przyczyny sprowadzają się do tego:
Stąd referencje.
źródło
Prosta odpowiedź:
Minimalizowanie zużycia pamięci
i
czasu procesora przy odtwarzaniu i wykonywaniu głębokiej kopii każdego obiektu, który gdzieś przeszedł.
źródło
W C ++ masz dwie główne opcje: zwracaj przez wartość lub zwracaj przez wskaźnik. Spójrzmy na pierwszy:
Zakładając, że twój kompilator nie jest wystarczająco inteligentny, aby użyć optymalizacji wartości zwracanej, dzieje się tutaj:
Zrobiliśmy kopię obiektu bez sensu. To strata czasu przetwarzania.
Zamiast tego spójrzmy na wskaźnik powrotu po wskaźniku:
Usunęliśmy zbędną kopię, ale teraz wprowadziliśmy kolejny problem: stworzyliśmy na stercie obiekt, który nie zostanie automatycznie zniszczony. Sami musimy sobie z tym poradzić:
Wiedza, kto jest odpowiedzialny za usunięcie obiektu przydzielonego w ten sposób, jest czymś, co można przekazać tylko za pomocą komentarzy lub konwencji. Łatwo prowadzi do wycieków pamięci.
Sugerowano wiele obejść w celu rozwiązania tych dwóch problemów - optymalizacja wartości zwracanej (w której kompilator jest wystarczająco inteligentny, aby nie tworzyć redundantnej kopii w wartości zwracanej przez wartość), przekazując odwołanie do metody (więc funkcja wstrzykuje do metody istniejący obiekt zamiast tworzenia nowego), inteligentne wskaźniki (tak, że kwestia własności jest dyskusyjna).
Twórcy Java / C # zdali sobie sprawę, że zawsze zwracanie obiektu przez referencję jest lepszym rozwiązaniem, szczególnie jeśli język obsługuje go natywnie. Łączy się z wieloma innymi funkcjami języków, takimi jak wyrzucanie elementów bezużytecznych itp.
źródło
Wiele innych odpowiedzi ma dobre informacje. Chciałbym dodać jeden ważny punkt dotyczący klonowania, który został tylko częściowo rozwiązany.
Używanie referencji jest mądre. Kopiowanie rzeczy jest niebezpieczne.
Jak powiedzieli inni, w Javie nie ma naturalnego „klonu”. To nie tylko brakująca funkcja. Nigdy nie chcesz po prostu nie chcąc * kopiować (płytko lub głęboko) każdej właściwości w obiekcie. Co jeśli ta właściwość była połączeniem z bazą danych? Nie można po prostu „sklonować” połączenia z bazą danych, tak jak nie można sklonować człowieka. Inicjalizacja istnieje z jakiegoś powodu.
Głębokie kopie same w sobie stanowią problem - jak głęboko naprawdę się posuniesz? Na pewno nie można skopiować niczego, co jest statyczne (w tym żadnych
Class
obiektów).Z tego samego powodu, dla którego nie ma naturalnego klonu, przedmioty przekazywane jako kopie stworzyłyby obłęd . Nawet gdybyś mógł „sklonować” połączenie DB - jak byś teraz upewniał się, że jest ono zamknięte?
* Patrz komentarze - Przez to „nigdy” oświadczenie rozumiem automatyczny klon, który klonuje każdą właściwość. Java nie dostarczyła go i prawdopodobnie nie jest to dobry pomysł dla Ciebie jako użytkownika języka z powodów wymienionych tutaj. Klonowanie tylko nieprzemijających pól byłoby początkiem, ale nawet wtedy trzeba by być ostrożnym przy określaniu,
transient
gdzie jest to właściwe.źródło
clone
jest w porządku. Przez „willy-nilly” mam na myśli kopiowanie wszystkich właściwości bez myślenia o tym - bez celowego zamiaru. Projektanci języka Java wymusili tę intencję, wymagając od użytkownika implementacjiclone
.Obiekty są zawsze przywoływane w Javie. Nigdy nie są omijani.
Zaletą jest to, że upraszcza to język. Obiekt C ++ może być reprezentowany jako wartość lub odwołanie, co powoduje konieczność użycia dwóch różnych operatorów w celu uzyskania dostępu do członka:
.
i->
. (Istnieją powody, dla których nie można tego skonsolidować; na przykład inteligentne wskaźniki to wartości, które są referencjami i muszą je rozróżniać.) Java potrzebuje tylko.
.Innym powodem jest to, że polimorfizm musi być wykonywany przez odniesienie, a nie wartość; obiekt traktowany przez wartość jest właśnie tam i ma ustalony typ. Można to zepsuć w C ++.
Ponadto Java może zmienić domyślne przypisanie / kopiowanie / cokolwiek. W C ++ jest to mniej lub bardziej głęboka kopia, podczas gdy w Javie jest to proste przypisanie wskaźnika / kopiowanie / cokolwiek, z
.clone()
i tak na wypadek, gdybyś musiał skopiować.źródło
const
można zmienić jego wartości, aby wskazywała na inne obiekty. Odwołanie to inna nazwa obiektu, nie może mieć wartości NULL i nie może być ponownie ustawione. Zasadniczo jest implementowany przez proste użycie wskaźników, ale jest to szczegół implementacji.Twoje początkowe stwierdzenie o obiektach C # przekazywanych przez odwołanie jest niepoprawne. W języku C # obiekty są typami referencyjnymi, ale domyślnie są przekazywane przez wartość, podobnie jak typy wartości. W przypadku typu referencyjnego „wartość” kopiowana jako parametr metody pass-by-value jest samą referencją, więc zmiany właściwości wewnątrz metody zostaną odzwierciedlone poza zakresem metody.
Jeśli jednak ponownie przypiszesz zmienną parametru do metody, zobaczysz, że zmiana ta nie zostanie odzwierciedlona poza zakresem metody. W przeciwieństwie do tego, jeśli faktycznie przekazujesz parametr przez odwołanie za pomocą
ref
słowa kluczowego, to zachowanie działa zgodnie z oczekiwaniami.źródło
Szybka odpowiedź
Projektanci języków Java i podobnych chcieli zastosować koncepcję „wszystko jest przedmiotem”. Przekazywanie danych jako referencji jest bardzo szybkie i nie zajmuje dużo pamięci.
Dodatkowy, nudny komentarz
Poza tym te języki używają odwołań do obiektów (Java, Delphi, C #, VB.NET, Vala, Scala, PHP), prawda jest taka, że odwołania do obiektów są wskaźnikami do ukrytych obiektów. Wartość null, przydział pamięci, kopia odwołania bez kopiowania całych danych obiektu, wszystkie są wskaźnikami obiektów, a nie zwykłymi obiektami !!!
W Object Pascal (nie Delphi), anc C ++ (nie Java, nie C #), obiekt może być zadeklarowany jako statycznie przydzielona zmienna, a także za pomocą dynamicznie przydzielonej zmiennej, poprzez użycie wskaźnika („odwołanie do obiektu” bez „ składnia cukru ”). Każdy przypadek używa określonej składni i nie ma sposobu, aby się pomylić, jak w Javie „i przyjaciele”. W tych językach obiekt można przekazać zarówno jako wartość, jak i jako odniesienie.
Programista wie, kiedy wymagana jest składnia wskaźnika, a kiedy nie jest wymagana, ale w Javie i podobnych językach jest to mylące.
Zanim Java istniała lub stała się głównym nurtem, wielu programistów nauczyło się OO w C ++ bez wskaźników, przekazując wartość lub referencję, gdy jest to wymagane. W przypadku przełączania z aplikacji do nauki na aplikacje biznesowe zwykle używają wskaźników obiektów. Biblioteka QT jest tego dobrym przykładem.
Kiedy nauczyłem się języka Java, starałem się podążać za koncepcją „wszystko jest przedmiotem”, ale pomyślałem o kodowaniu. W końcu powiedziałem „ok, to są obiekty przydzielane dynamicznie za pomocą wskaźnika ze składnią obiektu przydzielonego statycznie” i znowu nie miałem problemów z kodowaniem.
źródło
Java i C # przejmują kontrolę nad pamięcią niskiego poziomu. „Kupa”, w której znajdują się tworzone przez ciebie przedmioty, żyje własnym życiem; na przykład śmieciarz zbiera obiekty, kiedy tylko zechce.
Ponieważ istnieje oddzielna warstwa pośrednicząca między twoim programem a tą „stertą”, dwa sposoby odwoływania się do obiektu, według wartości i wskaźnika (jak w C ++), stają się nierozróżnialne : zawsze odwołujesz się do obiektów „wskaźnikiem” do gdzieś w kupie. Właśnie dlatego takie podejście projektowe sprawia, że przekazywanie przez odniesienie jest domyślną semantyką przypisania. Java, C #, Ruby i tak dalej.
Powyższe dotyczy tylko języków imperatywnych. We wspomnianych językach kontrola nad pamięcią jest przekazywana do środowiska wykonawczego, ale projekt języka mówi również „hej, ale tak naprawdę jest pamięć i są obiekty, które zajmują pamięć”. Języki funkcjonalne są jeszcze bardziej abstrakcyjne, wykluczając z definicji pojęcie „pamięci”. Właśnie dlatego przekazywanie przez odniesienie niekoniecznie dotyczy wszystkich języków, w których nie kontrolujesz pamięci niskiego poziomu.
źródło
Mogę wymyślić kilka powodów:
Kopiowanie typów pierwotnych jest banalne, zwykle przekłada się na jedną instrukcję maszynową.
Kopiowanie obiektów nie jest trywialne, obiekt może zawierać elementy, które same są obiektami. Kopiowanie obiektów jest kosztowne pod względem czasu procesora i pamięci. Istnieje nawet wiele sposobów kopiowania obiektu w zależności od kontekstu.
Przekazywanie obiektów przez odniesienie jest tanie i przydaje się również, gdy chcesz udostępnić / zaktualizować informacje o obiekcie między wieloma klientami obiektu.
Złożone struktury danych (szczególnie te rekurencyjne) wymagają wskaźników. Przekazywanie obiektów przez odniesienie jest po prostu bezpieczniejszym sposobem przekazywania wskaźników.
źródło
Ponieważ w przeciwnym razie funkcja powinna być w stanie automatycznie utworzyć (oczywiście głęboką) kopię dowolnego obiektu, który zostanie do niej przekazany. I zwykle nie da się tego zgadnąć. Musisz więc zdefiniować implementację metody kopiowania / klonowania dla wszystkich swoich obiektów / klas.
źródło
Ponieważ Java została zaprojektowana jako lepszy C ++, a C # został zaprojektowany jako lepszy Java, a twórcy tych języków byli zmęczeni fundamentalnie zepsutym modelem obiektowym C ++, w którym obiekty są typami wartości.
Dwie z trzech podstawowych zasad programowania obiektowego to dziedziczenie i polimorfizm, a traktowanie obiektów jako typów wartości zamiast typów odniesienia powoduje spustoszenie w obu przypadkach. Gdy przekazujesz obiekt do funkcji jako parametr, kompilator musi wiedzieć, ile bajtów przekazać. Kiedy twój obiekt jest typem odniesienia, odpowiedź jest prosta: rozmiar wskaźnika, taki sam dla wszystkich obiektów. Ale gdy obiekt jest typem wartości, musi przekazać rzeczywisty rozmiar wartości. Ponieważ klasa pochodna może dodawać nowe pola, oznacza to rozmiarof (pochodna)! = Rozmiarof (podstawa), a polimorfizm wychodzi poza okno.
Oto prosty program w C ++, który demonstruje problem:
Dane wyjściowe tego programu nie są takie same, jak w przypadku równoważnego programu w żadnym rozsądnym języku OO, ponieważ nie można przekazać obiektu pochodnego przez wartość do funkcji oczekującej obiektu podstawowego, więc kompilator tworzy konstruktor ukrytej kopii i przekazuje kopia nadrzędnej części obiektu podrzędnego , zamiast przekazywania obiektu podrzędnego, tak jak kazałeś to zrobić. Ukryte semantyczne błędy takie jak to, dlatego w C ++ należy unikać przekazywania obiektów według wartości i nie jest to możliwe w prawie każdym innym języku OO.
źródło
Ponieważ inaczej nie byłoby polimorfizmu.
W OO Programming możesz utworzyć większą
Derived
klasę zBase
jednej, a następnie przekazać ją do funkcji oczekującychBase
jednej. Całkiem trywialne prawda?Tyle że rozmiar argumentu funkcji jest stały i ustalany w czasie kompilacji. Możesz dyskutować, ile chcesz, kod wykonywalny jest taki, a języki muszą być wykonywane w jednym lub innym miejscu (języki czysto interpretowane nie są przez to ograniczone ...)
Teraz jest jeden kawałek danych, który jest dobrze zdefiniowany na komputerze: adres komórki pamięci, zwykle wyrażony jako jedno lub dwa „słowa”. Jest widoczny jako wskaźniki lub odniesienia w językach programowania.
Aby więc przekazać obiekty o dowolnej długości, najprostszą rzeczą jest przekazanie wskaźnika / odwołania do tego obiektu.
Jest to techniczne ograniczenie programowania OO.
Ale ponieważ w przypadku dużych typów i tak zazwyczaj wolisz przekazywać referencje, aby uniknąć kopiowania, nie jest to ogólnie uważane za poważny cios :)
Istnieje jedna ważna konsekwencja, w języku Java lub C #, gdy przekazujesz obiekt do metody, nie masz pojęcia, czy Twój obiekt zostanie zmodyfikowany za pomocą tej metody, czy nie. Sprawia to, że debugowanie / paralelizacja jest trudniejsza, i to jest problem, którym próbują się zająć języki funkcjonalne i transparentne referencje -> kopiowanie nie jest takie złe (jeśli ma to sens).
źródło
Odpowiedź jest w nazwie (i tak prawie). Odwołanie (jak adres) po prostu odnosi się do czegoś innego, wartość jest kolejną kopią czegoś innego. Jestem pewien, że ktoś prawdopodobnie wspomniał coś o następującym skutku, ale będą okoliczności, w których jedno, a nie drugie będzie odpowiednie (bezpieczeństwo pamięci vs. wydajność pamięci). Chodzi o zarządzanie pamięcią, pamięcią, pamięcią ... PAMIĘĆ! :RE
źródło
W porządku, więc nie mówię, że właśnie dlatego obiekty są typami referencji lub są przekazywane przez referencje, ale mogę podać przykład, dlaczego jest to bardzo dobry pomysł na dłuższą metę.
Jeśli się nie mylę, kiedy dziedziczysz klasę w C ++, wszystkie metody i właściwości tej klasy są fizycznie kopiowane do klasy potomnej. To tak, jakby pisać zawartość tej klasy ponownie w klasie podrzędnej.
Oznacza to, że całkowity rozmiar danych w klasie podrzędnej jest kombinacją elementów w klasie nadrzędnej i klasie pochodnej.
EG: #include
Co pokaże ci:
Oznacza to, że jeśli masz dużą hierarchię kilku klas, zadeklarowany tutaj całkowity rozmiar obiektu będzie kombinacją wszystkich tych klas. Oczywiście w wielu przypadkach obiekty te byłyby znacznie większe.
Wierzę, że rozwiązaniem jest utworzenie go na stercie i użycie wskaźników. Oznacza to, że w pewnym sensie można zarządzać rozmiarem obiektów klas z kilkoma rodzicami.
Dlatego używanie referencji byłoby bardziej preferowaną metodą do tego.
źródło