Translate: 
EnglishFrenchGermanItalianPolishPortugueseRussianSpanish

Silne typowanie danych w PHP, część II: autoboxing oraz niezniszczalne obiekty

We wcześniejszym artykule dotyczącym silnego typowania opisałem mechanizm typehintów, który wymusza zgodność typów danych (także nie będących obiektami) przekazywanych do metod i funkcji. Niestety wspomniana implementacja nie zabezpiecza przed innym problemem związanym z dynamicznym typowaniem zmiennych: brakiem kontroli typu podczas nadpisywania wartości zmiennych.

W celu zapewnienia kontroli typu, postanowiłem wprowadzić do języka PHP pojęcie autoboxingu znanego z języków C#, oraz Java.

Co to jest autoboxing?

Cytując za angielską wikipedią:

Autoboxing is the term for treating a value type as a reference 
type without any extra source code. The compiler automatically 
supplies the extra code needed to perform the type conversion.

Czyli w skrócie tłumacząc na polski: autoboxing pozwala na automatyczne opakowywanie typów prostych (integer, bool, float, string) w obiekty przy pomocy kompilatora.

Język PHP nie posiada wbudowanego wsparcia dla autoboxingu ani nie udostępnia prostej metody na nadzorowanie zmian nanoszonych na poszczególne zmienne (wyjątek: zmienne magiczne w obiektach). Na szczęscie z pomocą przychodzą nam w tym przypadku korzenie interpretera wyniesione z takich języków jak C i C++, czyli operacje na referencjach i wskaźnikach.

Skąd pobrać?

nominacja 9 miejsce

Gotową bibliotekę można pobrać z serwisu softpedia (bez logowania), lub phpclasses (wymaga zalogowania) lub bezpośrednio z repozytorium mojego blogu (nie wymaga logowania).

Jak rozbudować język PHP o autoboxing?

W tym celu musimy utworzyć tablicę przechowującą wskaźniki do zmiennych oraz metodę generującą nowy wskaźnik do deklarowanej zmiennej:

class VariablesManager
{
	protected static $counter;
 
	public static $memory = array();
 
	public static function & getNewPointer($dataType) {
		$name = ++self::$counter;
		VariablesManager::$memory[$name] = $dataType;
 
		if(is_object($dataType) && in_array('setPointer', get_class_methods($dataType))) {
			$dataType->setPointer($name);
		}
		return VariablesManager::$memory[$name];
	}
 
	private function __construct() { }
}

Tablica wskaźników znajduje się w zmiennej VariablesManager::$memory, a metodą odpowiedzialną za wygenerowanie wskaźnika jest VariablesManager::getNewPointer(). Metoda ta przyjmuje na wejściu instancję zmiennej prostej lub obiektu, która ma być opakowana.

W celu uniknięcia ewentualnych pomyłek, prywatny konstruktor wymusza zastosowanie klasy VariablesManager jako statycznej.

Posiadając już klasę główną, można skupić się na klasie, po której wszystkie opakowywane obiekty będą dziedziczyć. Tą klasą jest AutoBoxedObject:

abstract class AutoBoxedObject
{
	protected $ref;
 
	public function __destruct() {
		if($this->ref === null) {
			return;
		}
		if(VariablesManager::$memory[$this->ref] instanceof self) {
			VariablesManager::$memory[$this->ref]->setPointer($this->ref);
 
		} else if(is_scalar(VariablesManager::$memory[$this->ref])){
			$val = VariablesManager::$memory[$this->ref];
			$class = get_class($this);;
 
			VariablesManager::$memory[$this->ref] = new $class($val);
			VariablesManager::$memory[$this->ref]->setPointer($this->ref);
		}
	}
 
	public function setPointer($name) {
		$this->ref = $name;
	}
}

Jak widać klasa ta składa się z dwóch metod publicznych setPointer() oraz __destruct(), a także jednej zmiennej pomocniczej $ref przechowującej nazwę referencji do wskaźnika.

Szczegółowych wyjaśnień wymaga w tym przypadku destruktor klasy: jego głównym zadaniem jest sprawdzenie podczas niszczenia obiektu, czy obiekt ten jest po prostu usuwany ze zmiennej, czy też nadpisywany nową wartością. Sprawdzenie to odbywa się na zasadzie odczytu zmiennej, do której wskaźnik znajduje się w tablicy wskaźników VariablesManager::$memory.

W momencie nadpisywania zmiennej wskaźnik o nazwie $ref zapisany w tablicy $memory nie wskazuje już na bieżącą, niszczony właśnie obiekt $this, lecz na nową, docelową wartość. Pozwala to na pobranie tej wartości w ramach niszczenia starego obiektu, oraz jej natychmiastowe opakowanie w nowy obiekt tego samego typu.

Opórcz możliwości walidowania typu nowej wartości, pozytywnym efektem ubocznym jest też swoistego rodzaju niezniszczalność obiektu. Nadpisanie zmiennej zawierającej taki obiekt wartością typu prostego (np. integerem) spowoduje, że integer zostanie opakowany w nowy obiekt i nie będzie już typem prostym.

W klasie AutoboxingObject znajduje się także metoda setPointer(), jest ona wykorzystywana do przekazywania wskaźnika do nowo opakowanej wartości podczas niszczenia starego obiektu.

Jak skorzystać z autoboxingu?

Do stworzenia niezniszczalnego obiektu pozostało nam już tylko zaimplementowanie nowego typu danych oraz funkcji dostępowej, w tym przypadku niech będzie to klasa String:

class String extends AutoBoxedObject
{
	public $value;
 
	public function __construct($value) {
		$this->value = $value;
	}
 
	public function __toString() {
		return "$this->value";
	}
 
	public function toUpperCase() {
		return strtoupper($this->value);
	}
}
 
function & string($value = null) {
	$x = & VariablesManager::getNewPointer(new String($value));
	return $x;
}

Klasa String nie różni się niczym od standardowej klasy PHP. W celu implementacji autoboxingu wymagane jest spełnienie tylko dwóch warunków: należy dziedziczyć po klasie AutoboxingObject oraz wywoływać jego destruktor jeśli nadpisało się go własnym.

Funkcja dostępowa & string() ma za zadanie stworzyć nowy obiekt, stworzyć oraz zapamiętać wskaźnik do zmiennej w tablicy wskaźników, oraz przekazać go w ramach wyniku. Ważne jest to, że return $x nie zwraca instancji do nowego obiektu String, lecz jedynie wskaźnik do niej.

Jak to działa w praktyce?

Posiadając już wszystkie potrzebne klasy oraz funkcje, można w praktyce wypróbować działanie autoboxingu w PHP:

// przypisujemy zmiennej $y obiekt typu String
$y = & string("aaa");
// listujemy wlasciwosci obiektu
var_dump($y);
 
// teraz przypisujemy zmiennej $y typ prosty o wartosci "zzz"
$y = "zzz";
// po wylistowaniu powinnismy otrzymac typ prosty, pokazuje sie nam jednak obiekt
var_dump($y);
// ponizsza linijka zglosilaby blad krytyczny, gdyz "zzz" nie jest obiektem,
// dzieki autoboxingowi operacja sie powiedzie, gdyz zmienna $y nadal jest obiektem typu string
var_dump($y->toUpperCase());

Oto wynik wykonania skryptu:

object(String)#1 (2) {
  ["value"]=>
  string(3) "aaa"
  ["ref":protected]=>
  int(1)
}
object(String)#2 (2) {
  ["value"]=>
  string(3) "zzz"
  ["ref":protected]=>
  int(1)
}
string(3) "ZZZ"

Jak wymusić silne typowanie?

Posiadając mechanizm autoboxingu można to osiągnąć poprzez walidowanie danych przesyłanych do konstruktorów klas, które dziedziczą po klasie AutoboxingObject:

class Integer extends AutoBoxedObject
{
	public $value;
 
	public function __construct($value) {
		if(!is_int($value)) { throw new Exception("Invalid data type"); }
		$this->value = $value;
	}
}

Oczywiście w celu wymuszenia zgodności typów prostych, należy przygotować dla nich stosowne klasy, w które będą one opakowywane.

Gotowe skrypty implementujące tą funkcjonalność można ściągnąć z linków podanych na wstępie artykułu.

Podsumowanie

Mechanizm, który przedstawiłem powyżej posiada trzy główne zalety: wyposaża język PHP o brakujące sprawdzanie typów danych, umożliwia opakowywanie typów prostych w obiekty, a także nie wymaga instalowania po stronie serwera żadnych modułów PHP.

Kod implementujący silne typowanie napisałem w latach 2005-2006 dla potrzeb mojego frameworka oraz klasy String kompatybilnej z językiem Java, co sprawia że jest pierwszym i po dziś dzień jedynym funkcjonującym w języku PHP mechanizmem tego typu. Niestety nie doczekał się do tej pory żadnej konkurencji ze strony samych twórców języka, choć jak widać, nie jest to trudne do zaimplementowania, a czasami bardzo przydatne.

Czy silne typowanie danych w języku PHP jest przydatne? Od tej chwili można przekonać się o tym empirycznie.

Tagi: , , , , , , ,

Dodaj odpowiedź