Obiekty i referencje w PHP5

Mówiąc o modelu obiektowym w PHP5 i o głównych jego różnicach względem PHP4, często można spotkać się ze stwierdzeniem, że „obiekty przekazywane są domyślnie przez referencję, nie przez wartość”. Żeby nie być gołosłownym, przytaczam przykład z książki „Linux, Apache, MySQL i PHP. Zaawansowane programowanie” autorstwa m.in. J. Gernera i E.Naramore wydanej w Polsce przez wydawnictwo Helion:

W języku PHP 5 zmienne [odwołujące się do obiektu - przyp. vonski] są domyślnie przekazywane przez referencję.

Na wielu polskojęzycznych stronach traktujących o programowaniu obiektowym w PHP5 spotkałem się z podobną opinią. Prawda jest taka… że nie jest to do końca prawda ;)

Ogólnie o referencjach

Żeby zrozumieć o co chodzi w tym całym zamieszaniu, musimy sobie przypomnieć, czym są w ogóle referencje. Bardzo dobrze wyjaśnia to sam manual PHP:
Referencja jest aliasem pozwalającym dwóm różnym zmiennym odwoływać się do tej samej wartości.
Czyli generalnie sprawa jest prosta. Mając zmienną $a, której wartość wynosi 10 i przypisując ją przez referencję do zmiennej $b sprawiamy, że teraz $a i $b odwołują się do tej samej wartości (10). Czyli jeśli zmienimy tą wartość za pośrednictwem $b, również $a „odczuje” tego skutki, bo zmienna ta też odwołuje się do tej wartości (i vice versa).

Przykład #1. Prosta referencja
$a = 10;
$b = &$a;
echo "Zaraz po przypisaniu wartość zmiennej \$a to $a, a wartość zmiennej \$b to $b";
 
$b = 15;
echo "Teraz \$a = $a, \$b = $b";
 
$a = 20;
echo "A teraz \$a = $a, \$b = $b";
 
$b = null;
echo "I ostatecznie \$a = $a, \$b = $b";

Wynikiem działania tego kodu będzie:

Zaraz po przypisaniu wartość zmiennej $a to 10, a wartość zmiennej $b to 10
Teraz $a = 15, $b = 15
A teraz $a = 20, $b = 20 
I ostatecznie $a = , $b =

Jak widać wszystko się zgadza – nie ważne czy wartość zmieniamy za pośrednictwem $a czy $b, zawsze obie zmienne będą przypisane do tej samej wartości.

Referencja i obiekt

Jak to wygląda w przypadku obiektów? Czy faktycznie przypisywane są one domyślnie przez referencję? Z pozoru mogłoby się tak wydawać i myślę, że dlatego tak dużo źródeł uznaje z góry, że obiekty przypisywane są zawsze przez referencję. Tak jednak nie jest. Załóżmy, że tworzymy nowy obiekt klasy Foo. Robimy to oczywiście za pomocą operatora new: $o1 = new Foo();. I teraz uwaga. Zmienna $o1 nie przechowuje ani kopii obiektu, ani referencji do niego! Od tego momentu zmienna $o1 zawiera identyfikator obiektu, za pośrednictwem którego daje nam możliwość operowania obiektem. Co za tym idzie? Skoro wartością zmiennej $o1 jest identyfikator obiektu, przypisując tę wartość do zmiennej $o2, zmienna ta będzie zawierać kopię tego identyfikatora. Identyfikatora tego samego obiektu, do którego odwołuje się identyfikator przypisany do zmiennej $o1. Dlatego wykonując jakieś operacje na danym obiekcie za pośrednictwem zmiennej $o2, a potem odwołując się do tego obiektu za pośrednictwem zmiennej $o1, widzimy skutki naszych działań na obiekcie za pośrednictwem $o2. I stąd właśnie to złudne wrażenie, że w momencie przypisania $o2 = $o1 przypisaliśmy obiekt przez referencję. Właściwie na tym etapie można byłoby przyjąć, że tak faktycznie jest, ale… Jak widzieliśmy w pierwszym przykładzie, po przypisaniu przez referencję $a = &$b obie zmienne odwoływały się do tej samej wartości. Innymi słowy, $a stało się aliasem $b i odwrotnie. I w myśl tego jakąkolwiek wartość byśmy nie przypisali do $b, ta sama wartość „pojawiała się” w $a. Tak więc zakładając, że takie przypisanie obiektu $o2 = $o1 jest wykonywane domyślnie przez referencję, można się spodziewać tego, że obie te zmienne odwołują się do tej samej wartości, a co za tym idzie, zmieniając tę wartość za pośrednictwem jednej zmiennej, to samo „odczuje” druga zmienna. Problem w tym, że tak nie jest, co dowodzi tego, że obiekty w PHP5 nie są domyślnie przypisywane przez referencję.

Przykład #2. Dowód na to, że obiekty nie są przypisywane przez referencję
class Foo {}
 
$o1 = new Foo();
$o1->var = 10;
 
$o2 = $o1;
 
var_dump($o1);
var_dump($o2);
 
$o2->var = 20;
var_dump($o1);
var_dump($o2);
 
$o2 = 'coś innego';
var_dump($o1);
var_dump($o2);
Powyższy przykład wyświetli taki oto tekst:

object(Foo)#2 (1) { ["var"]=> int(10) }
object(Foo)#2 (1) { ["var"]=> int(10) }

object(Foo)#2 (1) { ["var"]=> int(20) }
object(Foo)#2 (1) { ["var"]=> int(20) }

object(Foo)#2 (1) { ["var"]=> int(20) }
string(10) "coś innego" 

Pierwsze przypisanie $o2 = $o1 tworzy zmienną $o2 której wartością jest kopia identyfikatora obiektu utworzonego za pomocą operacji new Foo(). Mamy więc w tym momencie dwa byty: oryginalny identyfikator, przypisany do $o1 oraz jego kopię znajdującą się w posiadaniu $o2. Obydwa identyfikatory wskazują do tego samego obiektu, dlatego dopóki wykonujemy operacje na obiekcie, nie ma to znaczenia, czy posługujemy się zmienną $o1, czy $o2 – zmianami zostanie dotknięty zawsze ten sam obiekt. I na tym etapie można odnieść wrażenie, że $o2 jest aliasem $o1. Wątpliwości (a raczej ich rozwianie) pojawiają się, gdy chcemy zmodyfikować nie obiekt, a samą wartość przechowywaną przez którąś z tych zmiennych. W powyższym przykładzie do zmiennej $o2 przypisujemy tekst „coś innego”, tym samym nadpisując kopię identyfikatora, którą przypisaliśmy zmiennej $o2 przy pierwszym przypisaniu. Jednak zmienna $o1 nadal przechowuje oryginalny identyfikator obiektu, ponieważ „nie wie” ona nic o tym, że $o2 zmieniła wartość. I tu jest nasuwa się wniosek. Tak naprawdę, przypisanie $o2 = $o1 jest zwykłym przypisaniem przez wartość. Trzeba się tylko zastanowić – co jest tą wartością? Otóż wartością jest owy identyfikator obiektu.
Zmodyfikujemy powyższy przykład, aby pokazać co się stanie jeśli przypiszemy $o1 do $o2 przez referencję, czyli zamiast zwykłego przypisania zastosujemy taki zapis: $o2 = &$o1.

Przykład #3. Przypisanie (identyfikatora) obiektu przez referencję.
$o1 = new Foo();
$o1->var = 10;
 
$o2 = &$o1;
 
var_dump($o1);
var_dump($o2);
 
$o2->var = 20;
var_dump($o1);
var_dump($o2);
 
$o2 = 'coś innego';
var_dump($o1);
var_dump($o2);
Po wykonaniu powyższego kodu, zobaczymy taki wynik:

object(Foo)#2 (1) { ["var"]=> int(10) }
object(Foo)#2 (1) { ["var"]=> int(10) }
object(Foo)#2 (1) { ["var"]=> int(20) }
object(Foo)#2 (1) { ["var"]=> int(20) }
string(10) "coś innego"
string(10) "coś innego" 

Jak widać, po zmianie wartości $o2 na „coś innego”, wartość $o1 również się zmieniła, ponieważ w tym przykładzie obie zmienne odnoszą się do tej samej wartości.
Dla pełnego zrozumienia zamieszczam jeszcze taką graficzkę.

Przykład #4. Grafika pokazująca różnicę między przypisaniem identyfikatora obiektu przez wartość i przez referencję Referencje - przykład
Mam nadzieję, że po dokładnej analizie powyższych przykładów, nikt już nie będzie miał wątpliwości, co do tego, w jaki sposób odbywa się przypisywanie obiektów do zmiennych. Trzeba po prostu zapamiętać, że każda zmienna przechowuje nie tyle obiekt, czy referencję do niego, co identyfikator obiektu, który jest niejako pośrednikiem między samą zmienną a obiektem (co obrazuje powyższa grafika).

, ,

One Response to “Obiekty i referencje w PHP5”

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

*

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">