PHP/Konstruktory i destruktory

< PHP
Poprzedni rozdział: Klasy i obiekty
Następny rozdział: Dziedziczenie

Konstruktory i destruktory

edytuj

Metody klas nie muszą być wywoływane wyłącznie przez programistę tworzącego dany skrypt. Istnieje pewna grupa metod, które są wywoływane automatycznie przez interpreter w momencie zajścia jakiegoś zdarzenia - metody takie nazywamy magicznymi, a w PHP możemy poznać je po tym, że ich nazwy rozpoczynają się od dwóch podkreśleń: __.

Pierwszymi magicznymi metodami, jakie poznamy, będą konstruktor i destruktor, wywoływane odpowiednio w momencie tworzenia oraz niszczenia obiektu.

Konstruktor

edytuj

Konstruktor jest metodą o nazwie __construct(), która może pobierać parametry, lecz nie wolno jej zwracać wartości. Jej zadaniem jest wykonanie pewnych akcji tuż po utworzeniu obiektu tak, aby można było od razu zacząć z nim pracę. Spójrzmy na nasz przykład z osobami, który analizowaliśmy ostatnio. Tuż po utworzeniu pola $_name oraz $_surname miały wartość pustą i należało ręcznie przypisać im wartość, a do tego czasu obiekt Person znajdował się w stanie, który możemy uznać za błędny. Może się zdarzyć, że wskutek pomyłki ktoś zapomni zainicjować odpowiednio obiekt po utworzeniu i przekaże go do dalszego przetwarzania. Gdy błąd się ujawni, moglibyśmy stracić dużo czasu na znalezienie błędu, a nawet potencjalnie zagrozić bezpieczeństwu aplikacji. Dzięki konstruktorom mamy pewność, że nasz obiekt zawsze będzie poprawnie inicjowany. W naszym przypadku chcemy, aby tworzona osoba od razu posiadała imię i nazwisko.

<?php
class Person
{
   private $_name = null;
   private $_surname = null;
 
   public function __construct($name, $surname)
   {
      $this->_name = $name;
      $this->_surname = $surname;
   } // end __construct();
 
   public function getName()
   {
      return $this->_name;            
   } // end getName();
 
   public function getSurname()
   {
      return $this->_surname;            
   } // end getSurname();
 
   public function getFullName()
   {
      return $this->_name.' '.$this->_surname;
   } // end getFullName();
} // end Person;

Settery są już nam niepotrzebne. Jak pamiętamy, mogły być one wywołane tylko raz, a skoro teraz imię i nazwisko jest przypisywane przez konstruktor, nie trzeba dodatkowych metod, które i tak nie zadziałają.

Popatrzmy teraz, jak przekazywać argumenty do konstruktora. Robimy to tuż po nazwie klasy przy operatorze new. Gdy klasa nie posiada konstruktora lub konstruktor nie pobiera argumentów, nawiasy wyjątkowo można w tym wypadku pominąć tak, jak to dotąd robiliśmy. Jednak po dokonanych zmianach musimy już napisać:

$janusz = new Person('Janusz', 'Kowalski');

Ćwiczenie: W poprzednim rozdziale podaliśmy zbudowany na OOP system konfiguracji. Jedną z klas wchodzących do zestawu była ConfigLoader. Dodaj do niej konstruktor, który pobiera nazwę z plikiem konfiguracyjnym tak, aby można go było podać już w momencie tworzenia obiektu. Przerób przykładowy kod tak, aby wykorzystywał możliwości konstruktora.

Destruktor

edytuj

Dotąd nie zajmowaliśmy się zagadnieniem niszczenia obiektów. Jest ono dość specyficzne, ponieważ PHP nie tylko nie narzuca obowiązku niszczenia niepotrzebnych obiektów, ale wręcz programiści często to ignorują. Większość skryptów PHP działa na zasadzie "uruchom się, wygeneruj odpowiedź, zakończ pracę", więc nie ma sensu niepotrzebnie komplikować skrypt i bawić się w zarządzanie cyklem życia obiektów, kiedy i tak wszystkie przestaną istnieć podczas kończenia pracy.

Podobnie jak w większości dynamicznych języków, obiekt przestaje istnieć w momencie usunięcia wszystkich prowadzących do niego referencji. W PHP mamy możliwość zaprogramowania operacji, która ma się wtedy wykonać, dzięki destruktorom. Przykładowo, gdy nasz obiekt reprezentuje otwarty plik, w destruktorze możemy go automatycznie zamknąć. Destruktor jest metodą o nazwie __destruct(), która nie może ani pobierać żadnych argumentów, ani też zwracać wartości. Poniższy przykład ilustruje działanie destruktorów:

<?php

class Destructable
{
   public function __construct()
   {
      echo 'Obiekt klasy Destructable został stworzony.<br/>';
   } // end __construct();

   public function __destruct()
   {
      echo 'Obiekt klasy Destructable został zniszczony.<br/>';
   } // end __destruct();
} // end Destructable;

$firstObject = new Destructable;
$secondObject = new Destructable;
unset($firstObject);

echo 'Kończymy pracę...<br/>';

Wynikiem jego działania jest:

Obiekt klasy Destructable został stworzony.
Obiekt klasy Destructable został stworzony.
Obiekt klasy Destructable został zniszczony.
Kończymy pracę...
Obiekt klasy Destructable został zniszczony.

Pierwsza z informacji o zniszczeniu obiektu powstała w wyniku jawnego wywołania unset($firstObject), które zniszczyło jedyną istniejącą w skrypcie referencję do niego. Drugi napis wygenerowała sekwencja kończenia pracy skryptu, podczas której kolejno niszczone są wszystkie obiekty.

Destruktory są wyjątkowymi metodami z powodu kilku dodatkowych ograniczeń, które ich dotyczą. Jeżeli wykonują się w momencie kończenia pracy skryptu, nie możemy z ich poziomu wysłać nagłówków HTTP, ponieważ te zostały już wysłane do przeglądarki. Ponadto niektóre serwery (np. Apache) zmieniają wtedy katalog roboczy, przez co wszystkie dotychczasowe ścieżki względne przestają wtedy działać. Rozwiązaniem jest wcześniejsze pozyskanie ścieżek bezwzględnych funkcją realpath() (uwaga: jest to operacja dyskowa i nie nadużywaj jej) i zapamiętanie ich do czasu zniszczenia obiektu.

Więcej o niszczeniu obiektów

edytuj

Podczas automatycznego niszczenia obiektów na podstawie licznika referencji pojawia się poważny problem. Przypuśćmy, że mamy obiekt A, który w jednym z pól przechowuje referencję do obiektu B. Jednocześnie B w swoim polu posiada referencję do obiektu A. W ogólnodostępnych zmiennych posiadamy jedną referencję do A, którą kasujemy, przez co oba obiekty stają się nieosiągalne dla naszego skryptu, lecz mimo to nie można ich usunąć, ponieważ liczniki wciąż wskazują, że istnieje do nich po jednej referencji. Problem ten nosi nazwę wykrywania cyklicznych referencji. Odśmiecacze pamięci większości języków (np. Java) potrafią poprawnie rozpoznawać takie sytuacje i mimo wszystko usunąć niedostępne obiekty, lecz PHP aż do wersji 5.3.0 pozbawiony był takiej możliwości. Większość skryptów wykonuje się krótko, dlatego zazwyczaj nikomu to nie przeszkadzało, jednak przy skomplikowanych, obiektowych strukturach danych, które w połowie działania trzeba było usuwać, aby zwolnić trochę pamięci dla reszty skryptu, programista musiał się nieźle nagimnastykować.

Sprawdźmy działanie poniższego skryptu:

<?php

class CircularReference
{
	private $_secondary;
	private $_name;
	
	public function __construct($name)
	{
		$this->_name = $name;
	} // end __construct();
	
	public function setSecondary(CircularReference $object)
	{
		$object->_secondary = $this;
		$this->_secondary = $object;
	} // end setSecondary();
	
	public function __destruct()
	{
		echo 'Obiekt '.$this->_name.' znika.<br/>';
	} // end __destruct();
} // end CircularReference;

// Tworzymy pierwszy obiekt i zapamietujemy referencje w $a
$a = new CircularReference('A');

// Dodajemy drugi obiekt, lecz nie zostawiamy sobie referencji
// Powstaje nam cykl: A ma dostep do B, B ma dostep do A.
$a->setSecondary(new CircularReference('B'));

// Usun jedyna posiadana referencje
unset($a);

echo 'Koniec pracy skryptu.<br/>';

Gdy uruchomimy ten skrypt, jego wynikiem działania powinno być:

Koniec pracy skryptu.
Obiekt A znika.
Obiekt B znika.

Czyli mimo, iż nie mamy do obiektu dostępu, nie jest on niszczony natychmiast. W PHP 5.3 automatyczne wykrywanie cykli może być włączone w pliku php.ini lub poprzez wywołanie funkcji gc_enable(), lecz najprawdopodobniej także i wtedy nasze wyjście będzie wyglądało tak, jak powyżej. Odśmiecacz po prostu czeka, aż uzbiera się wystarczająca liczba referencji i dopiero wtedy przegląda pamięć w poszukiwaniu obiektów. Możemy to wymusić, przerabiając lekko nasz skrypt. Aby PHP nie zawalił nas komunikatami o niszczonych obiektach, dodajmy flagę sygnalizującą, czy obiekt ma nas informować o swoim zniszczeniu, a następnie utwórzmy dodatkową pętlę, która będzie w kółko tworzyć obiekty z cyklami:

<?php
// Uwaga: tylko PHP 5.3.

class CircularReference
{
	private $_secondary;
	private $_name;
	private $_noMsg;
	
	public function __construct($name, $noMsg = false)
	{
		$this->_name = $name;
		$this->_noMsg = $noMsg;
	} // end __construct();
	
	public function setSecondary(CircularReference $object)
	{
		$object->_secondary = $this;
		$this->_secondary = $object;
	} // end setSecondary();
	
	public function __destruct()
	{
		if(!$this->_noMsg)
		{
			echo 'Obiekt '.$this->_name.' znika.<br/>';
		}
	} // end __destruct();
} // end CircularReference;

gc_enable();

// Tworzymy pierwszy obiekt i zapamietujemy referencje w $a
$a = new CircularReference('A');

// Dodajemy drugi obiekt, lecz nie zostawiamy sobie referencji
// Powstaje nam cykl: A ma dostep do B, B ma dostep do A.
$a->setSecondary(new CircularReference('B'));

// Usun jedyna posiadana referencje
unset($a);

// Tworz duzo obiektow z cyklami
for($i = 0; $i < 10000; $i++)
{
	$a = new CircularReference('A', true);
	$a->setSecondary(new CircularReference('B', true));
}

echo 'Koniec pracy skryptu.<br/>';

Tym razem odśmiecacz pamięci zareagował, co poznajemy po zmienionym wyniku:

Obiekt A znika.
Obiekt B znika.
Koniec pracy skryptu.

Spróbuj zmniejszyć ilość iteracji do 1000. Czy wtedy też włącza się odśmiecacz?

Oczywiście czasami nie chcemy czekać, aż PHP zorientuje się, że powinien wyczyścić pamięć. Odnalezienie cykli możemy wymusić, wywołując funkcję gc_collect_cycles(). Przerób powyższy skrypt, usuwając pętlę i zastępując ją wywołaniem tej funkcji. Zauważysz, że nasze obiekty ponownie zostały prawidłowo usunięte przed końcem pracy.

Zakończenie

edytuj

Umiemy już zarządzać tworzeniem oraz niszczeniem obiektu, a także wiemy, jak wykorzystać konstruktory do wymuszenia poprawnej inicjacji tworzonego obiektu. Ponadto poznaliśmy nieco zasady zarządzania pamięcią w PHP, które wprawdzie nie przydają się aż tak często, lecz na pewno warto je znać. W następnym rozdziale zajmiemy się dziedziczeniem.