Reference VS kopírování

Školení, která pořádám

Tento článek je překladem anglického originálu Reference vs Copying od Johana Perssona a vztahuje se k PHP 4. V PHP 5 se objekty vždy předávají referencí.

Sémantika referencí a kopírování

Toto je obvykle hlavním zdrojem chyb. I když si v PHP dokumentaci přečtete, že PHP 4 pro objekty používá kopírování místo referencí (jako většina ostatních OO jazyků), tak vás to nakonec stejně dostane nějakým nepředvídatelným způsobem.

Následující dvě sekce ukazují dva rafinované příklady, kde vás může kopírovací sémantika překvapit.

Důležitá věc, kterou musíte mít na paměti, je skutečnost, že proměnná s objektem není ukazatel na objekt, ale objekt sám. Většina problémů pramení z mylné představy, že přiřazovací operátor (=) vrátí alias objektu, přičemž ve skutečnosti vrátí novou kopii. Předpokládejme například, že $myObj je instance nějakého objektu a že má metodu Set(). Potom následující příklad nemusí udělat to, co by C++ (nebo Java) programátor očekával.

<?php
function SomeFunction($aObj) {
    $aObj->Set(10);
}

SomeFunction($myObj);
?>

Nyní bychom se mohli domnívat, že metoda Set() zavolaná ve funkci změní $myObj. Špatně.

Ve skutečnosti se stane to, že $myObj se zkopíruje do parametru $aObj, který se stane identickou kopií původního objektu. Když je potom zavolaná metoda Set(), tak ovlivní pouze místní kopii a neovlivní původní parametr $myObj.

K obměnám uvedeného problému může dojít kdekoliv, kde dojde k explicitnímu nebo implicitnímu (jako v uvedeném příkladě) přiřazení.

Aby funkce dělala to, co pravděpodobně očekáváte, musíte PHP říct, aby objekt předalo referencí, a to změnou deklarace na function SomeFunction(&$myObj).

Pokud znovu vyzkoušíte uvedený kód, zjistíte, že metoda Set() ovlivní původní parametr a to proto, že nyní jsme vytvořili alias na $myObj pojmenovaný $aObj.

Ale musíte dávat pozor, protože ani operátor & neřeší všechny problémy, jak bude uvedeno níže.

Získávání reference z reference

Řekněme, že máte následující kód:

<?php
$myObject = new SomeClass();
$myRefToObject = &$myObject;
?>

Co uděláme, pokud z nějakého důvodu chceme kopii vytvořené reference? Protože $myRefToObject už obsahuje referenci, někoho by lákalo napsat

<?php
$myCopyRefToObject = $myRefToObject;
?>

Bude to fungovat? Ne! Sémantika PHP vytvoří novou kopii objektu, kterého je $myRefToObject reference. Pokud chcete zkopírovat referenci objektu, musíte napsat

<?php
$myCopyRefToObject = &$myRefToObject;
?>

Např. v C++ obdoba tohoto příkladu vytvoří odkaz na odkaz. Ale v PHP to takhle nefunguje. Pro zkušeného C++ programátora je takovéto chování velice neintuitivní a může zapříčinit těžko odhalitelné chyby v PHP programu.

Dejte si pozor na implicitní (předáváním parametrů) nebo explicitní problémy způsobené tímto chováním.

Nejlepší způsob, jak se vyvarovat těchto sémantických „vychytávek“ je vždy předávat a přiřazovat objekty referencí. Jednak to zvýší rychlost (kopíruje se méně dat) a jednak to učiní sémantiku mnohem předvídatelnější.

Používání reference na $this v konstruktoru

Obvyklý vzor v konstruktoru objektu je inicializace nově vytvořeného objektu jako observer nějakého jiného objektu. Dá se to udělat např. následujícím způsobem:

<?php
class Battery {
    function Battery() { }
    function AddObserver($method, &$obj) {
        $this->obs[] = array($obj, $method);
    }
}
class Display {
    function Display(&$batt) {
        $batt->AddObserver("BatteryNotify", $this);
    }
    function BatteryNotify() { }
}
?>

V PHP 4 to ale nebude fungovat, pokud objekt inicializujete jako

<?php
$myBattery = new Battery();
$myDisplay = new Display($myBattery);
?>

Co je na tom špatně, je to, že volání new nevrátí stejný objekt jako ten, na který se odkazuje $this v konstruktoru. Místo toho se vrátí kopie nově vytvořeného objektu. Je to jiný objekt než původní objekt, který byl předán při volání AddObserver().

Když se třída Battery pokusí upozornit všechny své posluchače (zavoláním jejich upozorňovacích metod), nezavolá to náš nově vytvořený objekt $myDisplay, ale původní objekt označený jako $this. Takže když metoda BatteryNotify() změní nějaké vlastnosti objektu, tak se změní původní instance a ne – jak bychom mohli předpokládat – naše instance (která je kopií) třídy Display.

Aby to fungovalo, je potřeba PHP donutit, aby vracelo stejný objekt, který v konstruktoru reprezentovalo $this. To se zařídí přidání operátoru & při vytváření instance třídy Display takhle:

<?php
$myDisplay = &new Display($myBattery);
?>

Jako přímý důsledek této skutečnosti musí každý uživatel třídy Display vědět o jejích implementačních detailech! Ve skutečnosti lze argumentovat tím, že by s „nadbytečným“ operátorem & měly být vytvářeny všechny objekty. Co mohu říct, tak je to vždy bezpečné, ale jeho vynechání může mít nežádoucí účinky, jak bylo předvedeno výše.

V JpGraphu je pro vyřešení tohoto problému zvolen jiný přístup. Místo používání modifikovaného operátoru new používá objekt, který se potřebuje zaregistrovat, dvoufázové vytvoření použitím další metody Init(), která může bezpečně používat referenci na $this. (Pouze v konstruktoru se reference nechová tak, jak by se dalo předpokládat, protože new vrací kopii objektu.) Takže v uvedeném příkladě by to bylo implementováno takhle:

<?php
$myBattery = new Battery();
$myDisplay = new Display();
$myDisplay->Init($myBattery);
?>

Pro příklad tohoto obratu se podívejte na třídu LinerScale v jpgraph.php.

Jakub Vrána, Výuka, 27.3.2006, diskuse: 7 (nové: 0)

Diskuse

Ondrej Ivanic:

Davnejsie som cital taku pravdu o OOP v PHP4:

Nefunguje to tak ako ma?
Pridaj zopar &!
Nefunguje to stale?
Pridaj este viac &!

:)

ikona MiSHAK:

No určitě pak jde o zajímavé a přehledné řešení že?

Ondrej Ivanic:

Ako by som...

Kedze PHP4 pouziva kopirovanie objektov, tak ci tak sa dost vela veci sa musi robit trochu inac ako je to zvykom normalne. (t.j. podme sa lavou rokou skrabat na pravom uchu).

Odstranenim tych zopar & ktore su tam naviac sa urcite prehladnost neprida.

ikona MiSHAK:

Zkus se vžít do pozice člověka resp. programátora, který po tobě čte kód plný &. Ok já bych to dal... možná.

Jde spíš o "čistotu" špinavých peněz, něco jako zločin bez vraždy (člověka, nebo libovolného drobného a miloučkého zvířátka)...

pif:

jeden z duvodu, proc je php5 aspon lepsi zlo x))

ikona dgx:

Ještě přidám trik, jak se zbavit v PHP4 ampersandů http://www.dgx.cz/trine/item/php4-zbavme-se-ampersandu-ii

RATMex B:

Článok pre ľudí, ktorí si neprečítali časť "Language Reference" v manuáli, začali v PHP niečo písať a potom sa čudovali, že im to nefunguje, ako by chceli...

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.