Lekce 6 - Referenční a primitivní datové typy v PHP
V minulé lekci, První objektová komponenta v PHP - Galerie obrázků, jsme si vytvořili první objektovou komponentu, jednalo se o galerii obrázků.
Začínáme pracovat s objekty a objekty jsou referenčními datovými typy, které se v některých ohledech chovají jinak, než tzv. typy primitivní. Je důležité, abychom přesně věděli, co se uvnitř programu děje, jinak by nás v budoucnu mohlo leccos překvapit. Této problematice se budeme v dnešním PHP tutoriálu věnovat.
Primitivní datové typy
Proměnné primitivního datového typu jsme používali doposud, jedná se např. o číslo nebo o pole. I když primitivní typy v jiných jazycích označují pouze jednoduché struktury, jako jsou např. čísla nebo znaky (od toho název primitivní), v PHP je primitivním typem i pole nebo textový řetězec. Primitivní typy jsou vlastně všechny datové typy kromě objektů.
Kopírování hodnoty
Pokud někam předáme primitivní typ, jeho hodnota se vždy
zkopíruje. Ačkoli to určitě víte, zkusme si to. Založme si bokem
nějaký skript a vytvořme v něm nové pole s jednou hodnotou
56
:
$a = array(56);
Pole $a
nyní přiřaďme do pole $b
:
$b = $a;
Do pole $b
přidejme ještě další prvek:
$b[] = 28;
Nakonec vypišme obě pole:
{PHP}
$a = array(56);
$b = $a;
$b[] = 28;
print_r($a);
print_r($b);
{/PHP}
Když si nyní zobrazíme zdrojový kód výsledné stránky, výstup bude pravděpodobně tak, jak jste ho očekávali:
Ačkoli jsme změnili obsah proměnné $b
, její původní
hodnota zůstala zkopírovaná v proměnné $a
. Výsledkem jsou
tedy 2 různá pole, první jen s jednou hodnotou, druhé obsahující původní
hodnotu plus tu novou.
Referenční datové typy
S referenčními datovými typy, nebo chcete-li s objekty, je při předávání pracováno jiným způsobem. Zkusme si vytvořit podobnou situaci, ale místo čísel ukládejme objekty.
K tomu nám perfektně poslouží instance našich lidí. Vytvořme si
analogicky proměnnou $a
s instancí člověka:
require_once('tridy/Clovek.php'); $a = new Clovek('Jan', 'Novák', 30);
Nyní opět přiřaďme do proměnné $b
obsah proměnné
$a
:
$b = $a;
Opět po přiřazení změňme hodnotu proměnné $b
, zde tak,
že uživateli v proměnné $b
změníme věk:
$b->vek = 50;
Opět vypíšeme obě proměnné:
{PHP} require_once('tridy/Clovek.php'); $a = new Clovek('Jan', 'Novák', 30); $b = $a; $b->vek = 50; print_r($a); print_r($b); {/PHP}
{PHP} class Clovek { private $unava = 0; public function __construct(public string $jmeno, public string $prijmeni, public int $vek) {} public function spi(int $doba): void { $this->unava -= $doba * 10; if ($this->unava < 0) $this->unava = 0; } public function behej(int $vzdalenost): void { if ($this->unava + $vzdalenost <= 20) $this->unava += $vzdalenost; else echo('Jsem příliš unavený.'); } public function pozdrav(): void { echo('Ahoj, já jsem ' . $this->jmeno); } public function __toString(): string { return $this->jmeno; } }
A hle, něco je jinak. Měli byste vidět takovýto výsledek:
Rekapitulace
Vytvořili jsme si instanci člověka a tu jsme uložili do proměnné
$a
. Instance a proměnná jsou však 2 rozdílné věci a to z toho
důvodu, že instance není přímo uložená v proměnné.
Instance objektů jsou uložené v paměti, které se říká halda (anglicky
heap). Tato paměť není velikostně omezena (vejde se do ní kolik povolíte
nebo kolik máte RAM). Tuto paměť si můžeme jednoduše představit jako
velkou haldu objektů. V proměnné $a
je potom uložená pouze
tzv. reference, která ukazuje na instanci na haldě.
Jakmile jsme tedy do proměnné $b
přiřadili proměnnou
$a
, řekli jsme, že $b
ukazuje na tu samou instanci,
jako $a
. Celou situaci jsem pro vás vizualizoval, aby byla lepší
k pochopení:

Na obrázku je znázorněna halda s objekty a proměnné, které obsahují
referenci na tyto objekty. Vidíme, že proměnné $a
i
$b
ukazují na ten samý objekt. Z toho důvodu se změnil obsah
proměnné $a
ve chvíli, kdy jsme změnili $b
.
Člověka tam máme na rozdíl od předchozí situace s poli pouze
jednoho.
Práce s referencemi je přirozenější a mnohdy i rychlejší. Objekt
stačí vytvořit jednou a poté ho referencí předávat tam, kde je potřeba.
Když chcete v PHP použít pole na více místech, vždy se jeho obsah
kopíruje, což může být při větších datech nevýhodné. Předání
obrovského objektu je naprosto nenáročná operace, jelikož se předává jen
reference. Časem jistě uznáte i to, že je to pro člověka přirozenější
a aplikace se potom i lépe navrhuje. V realitě se nám totiž věci také
neklonují (většinou ).
Většina programovacích jazyků pracuje referenčně i s poli a textovými řetězci. Je to hlavně z toho důvodu, že primitivní typy jsou ukládané v paměti zvané zásobník (anglicky stack). Ta je velmi rychlá, ale její velikost je také velmi omezená. Složitější datové typy (obyčejně s neomezenou délkou) jsou vždy ukládány do haldy a ve stacku je umístěna pouze reference na ně. PHP tento fakt pravděpodobně vnitřně obchází a jako interpreter zachovává chování primitivních datových typů i u polí, i když jsou fyzicky také uložená na haldě.
Vynucení reference
Jak již bylo řečeno, PHP pracuje se vším kromě objektů jako s primitivním typem a to tak, že v případě předání hodnotu vždy zkopíruje. To je někdy výhodné, ale někdy zase nevýhodné. Z toho důvodu existuje možnost, jak předat proměnné primitivního typu (např. pole) referencí. Hned na úvod bych chtěl říci, abyste se použití těchto hacků spíše vyvarovali, jelikož mění princip jakým PHP funguje ve výchozím stavu a když na to zapomenete, může vás to někdy hodně překvapit a způsobit spoustu nepříjemností. Uvádím je zde tedy spíše pro úplnost.
Předání proměnné parametrem
Představte si, že máte funkci, po které chcete, aby něco přidala do pole, které předáte v jejím parametru. Naivní implementace by mohla vypadat takto:
// Tento kód nefunguje function pridej(array $pole, mixed $prvek): void { $pole[] = $prvek; }
Již pro vás však nebude překvapením, že tento kód do pole
$a
nic nepřidá:
{PHP}
// Tento kód nefunguje
function pridej(array $pole, mixed $prvek): void
{
$pole[] = $prvek;
}
$a = array(1, 2, 3);
pridej($a, 4);
print_r($a);
{/PHP}
Je to samozřejmě z toho důvodu, že pole $a
se zkopíruje do
proměnné $pole
a do té se přidá další prvek. Původní
proměnná $a
se však vůbec nezmění. U funkce můžeme vynutit
referenční přístup (jako u objektů) pomocí operátoru
&
:
{PHP}
function pridej(array &$pole, mixed $prvek): void
{
$pole[] = $prvek;
}
$a = array(1, 2, 3);
pridej($a, 4);
print_r($a);
{/PHP}
Kód bude nyní fungovat, nicméně je to spíše takový hack a není to příliš pěkné. Kdybychom napsali kód objektově (všimněte si, že je to funkce a ne metoda), tak bychom takové věci nepotřebovali. Pole by totiž bylo atributem tohoto objektu a nebyl by žádný problém s tím ho modifikovat. Navíc bychom si ušetřili atribut:
public function pridej(mixed $prvek): void { $this->pole[] = $prvek; }
Vidíme, jak objekty zjednodušují situaci a předchází nepřehlednému
kódu. Neobjektovým řešením by mohlo být vrátit modifikované pole pomocí
return
a poté ho přiřadit zpět do proměnné $a
.
Celé pole se nám tak ale již 2x zkopíruje, takže to není
nejvýhodnější.
Modifikace pole v cyklu
Vynutit referenci se nám může hodit v případě, kdy projíždíme pole
foreach
cyklem a potřebujeme měnit hodnoty. Tento kód nebude
fungovat:
{PHP}
// Tento kód nebude fungovat
$a = array(1, 2, 3);
foreach ($a as $prvek)
{
$prvek++;
}
print_r($a);
{/PHP}
Prvek pole se zkopíruje (pokud to není objekt) do
proměnné $prvek
a my měníme až tu, k samotnému prvku se
nedostaneme. S vynucením reference by to vypadalo takto:
{PHP}
$a = array(1, 2, 3);
foreach ($a as &$prvek)
{
$prvek++;
}
print_r($a);
{/PHP}
Prvek nyní ukazuje na původní prvek v poli a mění se tedy pole, nikoli
jen zkopírovaný prvek. Řešení je již méně matoucí než u funkce a
párkrát jsem ho použil, stále jde však obejít přes for
cyklus nebo vestavěné funkce, které nám PHP pro práci s poli nabízí.
V následujícím cvičení, Řešené úlohy k 4.-6. lekci OOP v PHP, si procvičíme nabyté zkušenosti z předchozích lekcí.