Notki oznaczone tagiem ‘referencje’

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:

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:

x = new Foo();

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

Foo y = x;
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?...

  • RSS
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

C#: referencje i porównywanie

2008-11-17 14:54

W C# obiekty zwykle dostępne są poprzez referencje. Zatem zmienna typu T (jeśli jest on klasą) nie zawiera samego obiektu T, lecz tylko odwołanie do niego. Porównując dwie takie zmienne przy pomocy operatora == domyślnie sprawdzamy więc, czy pokazują one na ten sam obiekt. Podobnie jest też przy użyciu domyślnej wersji metody System.Object.Equals; robi ona dokładnie to samo, co wspomniany operator. Można mądrze powiedzieć, że oba mechanizmy sprawdzają relację identyczności obiektów.

Czasami jednak chodzi nam o coś innego: chcemy sprawdzić, czy dwa obiekty są równe, np. w sensie zawartości swoich pól. Taka równość może zachodzić także wtedy, gdy obiektyte zostały stworzone zupełnie niezależnie. Znów można mądrze stwierdzić, że chcąc dokonać takiego sprawdzenia, realizujemy dla obiektów semantykę wartości. Co wtedy zrobić?... Ano przeciążyć tudzież nadpisać wspomnianą metodę Equals i/lub operator ==.
I tu zaczynają się schody, bo wcale nie jest łatwo zrobić to poprawnie, a jeszcze trudniej jest zrobić to sensownie. Wszystko zależy od tego, czy nasza klasa ma realizować wyłącznie ową nieszczęsną semantykę wartości czy też czasami będziemy jednak sprawdzać, czy dwie referencje pokazują na ten sam obiekt (a nie na dwa równe obiekty).

  • Jeśli tak (tzn. tylko sprawdzanie równości obiektów), to powinniśmy przeciążyć zarówno operator ==, jak i metodę Equals - i to przeciążyć tak, aby działały tak samo. Wtedy zwykle zajmujemy się najpierw operatorem, a później wykorzystujemy go w metodzie:
    struct Foo
    {
        private int field;

        public static bool operator == (Foo a, Foo b)    { a.field == b.field; }
        public override bool Equals(Object obj)
            { return obj is Foo && this == (Foo)obj; }
    }

    W takiej sytuacji prawdopodobnie powinniśmy też posłużyć się strukturą. Najpewniej chodzi nam bowiem o typ, który ma zachowywać się jak podstawowy: czyli coś w stylu wektora, macierzy, kwaternionu, itp.

  • W przeciwnym przypadku (czyli: czasem porównujemy wartości, a czasem referencje) klasa powinna pozostać klasą, lecz należy przedefiniować jej metodę Equals. To jej powinniśmy używać, do sprawdzenia relacji równości między obiektami. Kiedy zaś chcemy zwyczajnie porównać referencje, możemy wtedy uciec się do operatora ==.

Uff, całkiem to zawiłe, prawda? Niestety nie jest łatwo uchwycić różnicę między równością a identycznością - a w językach, które obiekty realizują przez referencję sytuacja komplikuje się jeszcze bardziej. Zaś już chyba całkiem rozmywa się wtedy, gdy niektóre typy traktujemy per wartość, a niektóre per referencja...
A tak przecież jest C#. I jednocześnie podobno to jeden z prostszych języków do nauki i użytkowania :D

  • RSS
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

Wskaźniki i referencje jako parametry

2008-07-09 19:07

Kiedy w C++ chcemy przekazać do funkcji odwołanie do obiektu (zezwalające na jego modyfikację wewnątrz funkcji), mamy do wyboru dwie metody. Ta alternatywa to posłużenie się wskaźnikiem albo referencją:

void Function(Foo* pFoo);
void Function(Foo& foo)

Czy istnieje uniwersalna odpowiedź na to, którą wybrać? Chyba nie. Jeśli chodzi o wskaźnik, to za jego użyciem może przemawiać:

  • Możliwość przekazania odwołania pustego, jeśli parametr nie jest obowiązkowy. Obejmuje to oczywiście zdefiniowanie NULL jako domyślnej wartości dla tego parametru w deklaracji funkcji. Nie jest to możliwe dla referencji (w C++).
  • Fakt, że w wywołaniu funkcji bardziej widoczne jest to, iż przekazany do niej za pośrednictwem wskaźnika obiekt może się zmienić. Jeśli na przykład obiekt ten jest zmienną lokalną, to konieczne jest posłużenie się operatorem &, który daje o tym jakąś widoczną wskazówkę (nie tak jasną jak ref/out w C#, ale zawsze). Nie jestem też wielkim fanem notacji węgierskiej, lecz w przypadku wskaźników stosowanie przedrostka p wydaje mi się akurat wskazane i w tym kontekście też zwiększa czytelność wywołania funkcji, wskazując, że przekazywany obiekt (alokowany na stercie) też może się zmienić.
    Foo foo; Foo* pFoo = new Foo();
    Function (&Foo); Function (pFoo); // funkcja może zmienić obiekt

Z kolei referencje mogą się popisać innymi zaletami:

  • Nie można do nich przekazać odwołania pustego. To może być zaletą, jeśli taka sytuacja jest niepożądana. Ponadto brak konieczności sprawdzania tego, czy referencja jest "pusta", może lekko poprawić wydajność kodu generowanego przez kompilator.
  • Składnia użycia obiektu przekazywanego przez referencję zwykle bywa bardziej przejrzysta. Jest tak zwłaszcza wtedy, gdy używamy względem niego operatorów. Na przykład kolekcja dostępna przez wskaźnik musiałaby być indeksowana przez (*pArray)[i], zaś przez referencję po prostu jako array[i].

Widać więc, że jeśli kwestia odwołania pustego nie jest dla nas istotna, to decyzja może być trudna. Ale naturalnie jest tak tylko wtedy, gdy zechcemy się nad takimi sprawami zastanawiać ;]

  • RSS
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

Tajemnice stałych referencji

2008-03-13 21:47

Zapewne wielu programistów C++ zetknęło się z taką lub podobną sytuacją. Oto w pocie czoła napisana funkcja w postaci podobnej do tej:

void Foo(string s) { /* ... */ }

zostaje obejrzana przez bardziej doświadczonego kodera, który stwierdza: "Powinieneś użyć tutaj stałej referencji jako parametru - czyli napisać:

void Foo(const string& s)

Unikniesz wtedy niepotrzebnego kopiowania stringa". Wówczas możemy zrobić wielkie oczy i zdziwić się bardzo, zwłaszcza jeśli wiemy co nieco o referencjach w C++. Mimo to sugestia ta jest jak najbardziej na miejscu i powinniśmy się do niej stosować. Wstyd przyznać się, że przez dłuższy czas niesłusznie brałem ją tylko na wiarę, lecz na szczęście jakiś czas temu dowiedziałem się dokładnie, o co tutaj chodzi. Tą cenną wiedzą oczywiście się podzielę :)

Wiadomo, że w C++ referencje to w zasadzie to samo, co (raczej rzadko używane) stałe wskaźniki - czyli zmienne w typu T* const. Różnią się one aczkolwiek składnią: wszystkie dereferencje są dokonywane przezroczyście i kod wygląda tak, jakbyśmy posługiwali się zwykła zmienną docelowego typu, na który referencja wskazuje (czyli T). Podobnie stałe referencje są odpowiednikami stałych wskaźników na stałe (const T* const), czyli takich, które nie pozwalają ani na modyfikację obiektu wskazywanego, ani na zmianę samego wskazania.
W każdym przypadku wskaźniki muszą jednak na coś pokazywać; na coś, co ma określone miejsce w pamięci, czyli adres. W zasadzie podobna reguła dotyczy też referencji - z jednym małym, ale jakże ważnym wyjątkiem.

Otóż powyższą funkcję (w wersji ze stałą referencją jako parametrem) możemy bez problemów wywołać tak, jak poniżej:

Foo ("Hello world");

Niby nic nadzwyczajnego, ale zauważmy, że tworzony jest tutaj tymczasowy obiekt string, na który następnie pokazuje referencja w ciele naszej funkcji. Błąd standardu lub kompilatora? Wręcz przeciwnie - it's not a bug, it's a feature :)
Po prostu w C++ stałe (i tylko stałe) referencje mogą poprawnie wskazywać na obiekty tymczasowe. Życie takich obiektów jest wówczas przedłużane aż do czasu wyjścia poza zasięg istnienia referencji. W naszym przypadku utworzony tymczasowy obiekt string będzie więc dostępny w całej funkcji.

Biorąc pod uwagę fakt, że zamiana deklaracji string s na const string& s nie kosztuje nas nic (wewnątrz funkcji do parametru odwołujemy się tak samo), możemy zerowym kosztem zyskać sporą optymalizację. Przekazanie referencji kosztuje przecież tyle samo co przekazanie wskaźnika i na pewno jest nieporównywalnie tańsze niż wykonywanie kopii całego napisu.
Dlatego też nie tylko w przypadku klasy string, ale i we wszystkich podobnych sytuacjach obiekty powinniśmy przekazywać właśnie przez stałe referencje.

  • RSS
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

Referencje do klas w Delphi

2007-08-20 11:30

W ramach kontynuacji przeglądu nietypowych konstrukcji językowych - który to nieopatrznie rozpocząłem, zajmując się pętlami w Pythonie - obejrzymy sobie jeden z elementów języka Object Pascal. Są to referencje do klas, zwane też czasem metaklasami.

Ten dziwny twór działa jak odwołanie wskazujące na klasę jako typ, a nie na jej konkretny obiekt. Deklaruje się go mniej więcej w taki sposób:

type TClass = class of TObject;

Zmienne należące do tak zdefiniowanego typu TClass mogą pokazywać na wszystkie klasy dziedziczące po TObject. Innymi słowy, takie zmienne są swego rodzaju dynamicznymi aliasami na nazwy klas; używając ich, nie musimy nawet wiedzieć, z jakiego typu klasą konkretną mamy do czynienia. Przypomina to oczywiście normalny dynamiczny polimorfizm obiektów, osiągany przy pomocy funkcji wirtualnych. Tutaj jest to niejako dynamiczny polimorfizm samych klas.

Użycie takiego typu referencyjnego może wyglądać choćby tak:

type TFoo = class(TObject)   // klasa dziedzicząca po TObject
// ...
end;

var
  AnyClass : TClass;    // zmienna będąca referencją do klasy
  AnyObject : TObject;   // zwykłe odwołanie do obiektu
begin
  AnyClass := TFoo;   // referencja pokazuje na klasę TFoo
  AnyObject := AnyClass.Create;   // tworzy obiekt klas TFoo przy pomocy referencji
end;

Ten przykład pokazuje, że w Delphi przy pomocy referencji do klas możliwe jest łatwe zrealizowanie wzorca wirtualnego konstruktora. Nie musimy bowiem wiedzieć, na jaką klasę wskazuje referencja, a utworzony obiekt możemy "odebrać" posługując się zmienną typu bazowego (tutaj TObject).

Co na to C++? Nie ma tam naturalnie podobnej konstrukcji. Zbliżone do niej - w sensie możliwości korzystania z jakiegoś typu bez wiedzy, czym on naprawdę jest - są parametry szablonów. Podstawowa różnica polega jednak na tym, że szablony są rozwijane w trakcie kompilacji i "wartości" tych parametrów są niezmienne.
Nie wiem, czy można w jakiś sposób zaimplementować w C++ metaklasy o funkcjonalności zbliżonej do powyższej. Znając możliwości C++, to całkiem prawdopodobne :) Ich ewentualny brak nie były jednak jakoś szczególnie dotkliwy, gdyż większość ich zastosowań z powodzeniem daje się zastąpić szablonami lub zwykłymi funkcjami wirtualnymi.

  • RSS
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks
 



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