Dlaczego nie lubię typów referencyjnych

2009-12-16 11:54

Jeśli chodzi o C++, to nietrudno zauważyć, że często pozwalam sobie na sporo uwag krytycznych pod adresem tego języka. Oczywiście zawsze jest to krytyka konstruktywna :) Tym niemniej wiele jest tu rzeczy, o których można powiedzieć, że w innych językach zostały pomyślane lepiej (łącznie z takimi, których w C++ nie ma, a przydałyby się).
Dlatego dzisiaj będzie trochę nietypowo. Chcę bowiem wspomnieć o problemie, który w językach pokroju C# czy Javy potrafi doprowadzić do powstawania trudnych do wykrycia błędów – i który jednocześnie w C++ w zasadzie nie występuje wcale.

Mam tu na myśli semantykę referencji, czyli pewien szczególny sposób odwoływania się do szeroko rozumianych obiektów w kodzie. Klasy, a właściwie to prawie wszystkie typy poza podstawowymi (jak liczby czy znaki), są w C# i Javie obsługiwane w ten właśnie sposób; dlatego czasami nazywa się je typami referencyjnymi.
Najważniejszą cechą takich typów jest fakt, że należące do nich zmienne nie zawierają bezpośrednio instancji obiektów. Jeśli na przykład Foo jest klasą, to deklaracja:

  1. Foo x;

nie sprawi, że pod nazwą x będzie siedział obiekt typu Foo. x będzie tutaj zaledwie odwołaniem do takiego obiektu – w tym przypadku zresztą odwołaniem pustym, niepokazującym na nic.
Jest to zachowanie diametralnie różne od typów podstawowych, jak choćby int. Ale idźmy dalej – skoro mamy zmienną mogącą trzymać odwołanie (czyli referencję) do obiektu, to pokażmy nią na jakiś obiekt, na przykład taki zupełnie nowy:

  1. x = new Foo();

A że w prawdziwym programie zmiennych i obiektów jest zawsze mnóstwo, to wprowadźmy na scenę jeszcze parę:

  1. Foo y = x;
  2. y.SomeValue = 4; // hmm...

No i zonk, można powiedzieć… Nikt aczkolwiek tego nie powie, bo dla każdego programisty C#, Javy itp. istnienie wielu referencji do tego samego obiektu jest rzeczą całkowicie naturalną. Jednak wiem, że podobny kod dla dowolnego typu liczbowego (zastąpiwszy ostatnią linijkę przez y += 4; lub coś tym w guście) zachowałby się zupełnie inaczej. Wiem też, że kiedyś byłem zmuszony wykonać kilka empirycznych testów, by się o tym naocznie przekonać; było to jeszcze w Delphi, a powodem były oczywiście jakieś “dziwne” błędy, na które natrafiłem w jednym ze swoich programów. Źle użyte typy referencyjne łatwo mogą być bowiem przyczyną takich błędów, które zresztą bywają potem trudne do wykrycia.

Bez jakiegoś rodzaju referencji nie da się rzecz jasna wyobrazić sobie użytecznego języka programowania. Sęk w tym, że w C# czy Javie używanie ich nie jest opcją do stosowania w tych przypadkach, które tego wymagają – jest koniecznością wymuszoną przez sam fakt programowania z użyciem klas i obiektów. To całkiem inaczej niż w C++, gdzie w tym celu trzeba wyraźnie zaznaczyć swoje intencje (najczęściej poprzez użycie typów wskaźnikowych).
W tworzeniu oprogramowania istnieje tzw. zasada najmniejszego zdziwienia (principle of least astonishment). Mówi ona, że przy alternatywie równoważnych przypadków powinno się wybrać ten, który u użytkownika końcowego będzie powodował mniejsze zdziwienie. Czy typy referencyjne zachowujące się zupełnie inaczej niż typy podstawowe i “same” zmieniające swoją zawartość nie są przypadkiem złamaniem tej reguły?…

Be Sociable, Share!
Be Sociable, Share!
Tags: , , ,
Author: Xion, posted under Programming, Thoughts »


12 comments for post “Dlaczego nie lubię typów referencyjnych”.
  1. Kauach:
    December 16th, 2009 o 14:27

    Też się wiele razy nacinałem na typy referencyjne i trzeba się do nich przyzwyczaić, ale mówienie, że nieintuicyjne jest nieco przesadzone. Traktują każdy obiekt klasy jako wskaźnik na ten obiekt i jest git – problemów nie ma. Jak chcę użyć typu niereferencyjnego to deklaruję strukturę. Zdaję sobie sprawę, że struktura ma jeszcze parę ograniczeń, ale w 99% przypadków te dwie rzeczy w zupełności wystarczają. Kwestia przyzwyczajenia do specyfiki języka i tyle. Jakbyś zaczął (nie daj Boże) od javy to podejście z C++ byłoby dla Ciebie zaskakujące.

  2. Kos:
    December 16th, 2009 o 19:26

    Kwestia punktu siedzenia. Ktoś, kto zaczynał od Javy lub C#, przejście na C++ byłoby równie ciężkie, bo ktos napisalby w C++:

    1. CFoo obiekt = new CFoo(1,2,3);
    2. jakiesMiejsce->jakiesPole = obiekt;

    I zrobi wielkie WTF, bo dowie się że obiekt został skopiowany. Czemu został skopiowany? Przecież nigdzie nie ma .clone() ani niczego podobnie brzmiącego. Mamy jedno “new”, a dwa obiekty – co jest?

    Co ciekawe, wcale nie musi być to rozumiane jako zachowanie inne, niż przy typach podstawowych. Wystarczy zmienić nieco tok myślenia:
    1) Zapomnijmy na chwilę, że zmienne to są jakieś kawałki pamięci z zerami i jedynkami w środku. Niech każda zmienna lokalna czy pole klasy będzie w naszym rozumieniu uchwytem, referencją do czegoś.
    2) Załóżmy ciągłe istnienie nieskończenie wielu obiektów typów podstawowych, do których mamy w dowolnym momencie dostęp poprzez referencje-literały typu 123 czy “tekst”. Wszystkie te obiekty należą do jakiejś wbudowanej w język niby-klasy z ważną właściwością – są zupełnie niezmienne.

    Spójrzmy teraz jeszcze raz na Twój kod, który powodowałby wieloznaczność (załóżmy Javę):

    1. int x = 5;
    2. int y = x;
    3. x += 4; // hmm...

    Po kolei, patrząc przez nasze nowe kolorowe okulary: Pierwsza linijka tworzy referencję do inta i ustawia ją na ten sam obiekt, do którego wskazuje referencja ‘5’. Druga tworzy nową referencję do inta i każe jej pokazywać na ten sam obiekt, co pierwsza.
    Trzecią linijkę rozumiemy po prostu jako x = x+4, czyli coś w stylu “x = x.plus(4)” gdzie plus niech będzie metodą naszej nibyklasy “int”.

    Fakt, że zupełnie inny tok myślenia, niż w C++ (tu zmienna = referencja do czegoś, tam zmienna = kawałek pamięci), ale jeśli rozumujemy w ten sposób, to wszystko jest spójne. Ba – jeśli kodujemy w np. pythonie, to taki tok myślenia się bardzo gładko przekłada na język, bo nikt tam nie broni napisać czegoś w stylu “a = 5.__add__(3)” :-)

  3. Anonymous:
    December 16th, 2009 o 22:41

    Jeżeli pisze się tylko w C++ to się człowiek przyzwyczaja do semantyki tego co pisze, jeżeli pisze tylko w C#/Javie to również. Problem pojawia się jeżeli się piszę i w tym i w tym :} Musze przyznać, że mimo, że naprawdę sporo piszę w Javie (w c++ mniej) to musiałem się dłużej zastanowić co tak naprawdę się stanie w linii “Foo y = x;”, prawdopodobnie dlatego, że ostatnio pisałem w c++.

    A pisałem w C++ bo brakowało mi pewnej rzeczy w Javie. Nie wiem czy to dobre miejsce, ale skoro już mowa o typach referencyjnych to pozwolę sobie przedstawić mój problem.
    Otóż implementowałem drzewo BST i chciałem napisać w Javie metodke która by miała sygnature mniej więcej “Node find(T key, Node parent)” – wynikiem jej działania miało być:
    1. znalezienie węzła o podanym w zmiennej key kluczu
    2. przypisanie do zmiennej parent referencji na rodzica węzła
    3. zwrócenie znalezionego węzła
    Powodem dla którego nie napisałem tego w Javie była nie możliwość przypisania do zmiennej referencyjnej (parent) referencji na obiekt, tak by nie przypisanie nie dotyczyło tylko zmiennej zadeklarowanej jako parametr metody, ale by dotyczyło zmiennej będącej przekazanej do metody jako argument.
    W C++ jest to tak proste jak użycie mechanizmu referencji (czy odnośników jak niektórzy wolą go nazywać), czyli sygnatura tej metody w C++ wygląda tak: “Node* find(T key, Node*& parent)”.
    Moje pytanie brzmi: Czy aby na pewno w Javie nie da się tego zrobić ?? A jeżeli się da, to czy jest to proste, czy wymaga dużej ilości ‘kombinowania’??

  4. Aithne:
    December 16th, 2009 o 23:15

    Tak to już jest, kiedy się nazwie wskaźniki “referencjami”… Czy kogoś dziwi, że po

    1. Foo* foo = new Foo();
    2. Foo* bar = foo;

    mamy jeden obiekt i dwa odwołania do niego? W przypadku C# czy Javy mamy przecież dokładnie to samo, tylko ukradli nam gwiazdkę, jest kropka zamiast strzałki i nie mamy arytmetyki wskaźników…

  5. Xion:
    December 16th, 2009 o 23:36

    Aithne: Nikogo to nie dziwi właśnie dlatego, że mamy tutaj wskaźniki i T* jest innym typem niż zwykłe T.

  6. Aithne:
    December 17th, 2009 o 21:11

    Czyli dokładnie jak w Javie i podobnych językach, z tym, że w nich mamy tylko T* zapisywane jako T, zaś do “zwykłego T” to się nijak nie możemy dostać. Zachowanie tych języków jest całkowicie logiczne i zgodne z oczekiwaniami, jeśli tylko się pamięta, że działamy na wskaźnikach.

  7. Xion:
    December 17th, 2009 o 22:10

    Czy wobec tego int z Javy jest równoważny int* z C++?…

  8. Aithne:
    December 18th, 2009 o 14:03

    Jeśli rozumować jak Kos, to można powiedzieć że tak.

  9. Kauach:
    December 18th, 2009 o 20:45

    no niestety i w Javie i w C# trzeba pamiętać co jest typem prostym/strukturą, a co nie.

    tak samo jak w bardziej złożonym kodzie c++ trzeba pamiętać co jest wskaźnikiem a co nie… szczególnie jak ktoś stosuje typedefy, bo nazwa typu LPSHORT do mnie nie przemawia nijak.

  10. Aithne:
    December 19th, 2009 o 17:38

    LPSHORT – Long Pointer to SHORT. To “long” to pozostałość z czasów, gdy były takie rzeczy jak bliskie i dalekie wskaźniki. Teraz już bardziej przemawia? ;)

  11. Reg:
    December 20th, 2009 o 13:50

    Ciekawostką jest, że w PHP tablice są przypisywane i przekazywane… przez wartość, nie przez referencję. Dzięki temu funkcja może modyfikować swoją lokalną kopię przekazanej jej tablicy, co ostatnio mi się przydało.

  12. Kauach:
    December 20th, 2009 o 18:47

    Aithne – ja ROZUMIEM co to jest i dlaczego. Nie ma to nic wspólnego z PRZEMAWIANIEM do mnie =). Tego typu nazewnictwo zawsze doprowadzało mnie do szału =)

Comments are disabled.
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.