V tomto jubilejním dvaatřivátém .NET Tipu se podíváme na zoubek přiřazování, tedy operátoru =. Možná si říkáte, že na tom přece nemůže být nic nejasného, je to věc, se kterou se u programování setkáte prakticky hned na začátku, bez něj se toho totiž nedá nic moc udělat. Nedávno tady ale padl relativně zajímavý dotaz na fóru a bylo z něj vidět, že to není úplně zřejmé, zvláště když se přiřazují struktury a třídy. Jak to tedy je?
Přiřazování
Na pravé straně operátoru přiřazení stojí vždycky výraz, na levé straně pak proměnná (případně vlastnost, ale představme si pro chvíli vlastnost jako proměnnou). Přiřazení nejprve výraz na pravé straně vyhodnotí a nahradí hodnotu proměnné výsledkem tohoto výrazu.
Pokud je proměnná typu hodnotového (číselné datové typy, struktura nebo enum, což je jen “maskované” číslo), je to jasné, proměnná je prostě přímo kus paměti, ve které je její hodnota uložená. U Integeru jsou to 4 bajty, ve kterých je uložené číslo, u Double je to bajtů 8 atd. Pokud se jedná o strukturu, je to kus paměti, ve kterém jsou všechny její vnitřní proměnné, povětšinou naskládané za sebou a případně dorovnané na nějakou rozumnou velikost (typicky násobek 4 bajtů, tedy 32 bitů, to bývá u 32bitových procesorů nejefektivnější). Pokud tedy přiřazujeme strukturu, její blok paměti se prostě zkopíruje.
Pokud je proměnná typu referenčního, obsahuje jen tzv. referenci, což je adresa v paměti, kde data skutečně leží. Referenčním typem jsou instance tříd, např. pole, String, FileStream, Exception, Form atd. Přiřazením proměnné a do proměnné b (obě jsou stejného referenčního typu) se prostě jen zkopíruje adresa, ne data objektu. Obě proměnné tedy budou obsahovat stejný objekt, pokud v něm něco změníte, změní se to i ve druhé proměnné, obě odkazují na tentýž objekt. Tady je docela důležité uvědomit si, co je to změna objektu.
Změnou objektu se myslí udělání čehokoliv, co změní obsah paměti objektu, typicky nějakou jeho vnitřní proměnnou. Nejčastěji to bývá přiřazení hodnoty do vlastnosti, přímé nastavení jeho proměnné (pokud je public, což by se ale nemělo) nebo zavolání nějaké metody, která opět změní jeho vnitřní proměnnou. Pokud je objektem pole, přiřazení hodnoty do jeho buňky je též změna objektu, změní se tím data uvnitř objektu.
Změna objektu ovšem není přiřazení jiného objektu do proměnné. To totiž s objektem, který byl v proměnné původně, nic neudělá, tedy alespoň přímo. Pokud proměnná byla poslední, která měla referenci na původní objekt, který tam byl, přiřazením jsme na něj ztratili tuto poslední referenci a časem, až se spustí Garbage Collector, tak původní objekt zruší. Neexistuje na něj již žádná platná reference a tím pádem jej již nikdy nemůžeme použít, nedá se k němu nijak dostat.
Jeden příklad na ujasnění:
Dim v As New Vector()
Dim w As Vector
' nastavíme první vektor
v.X = 15
v.Y = 20
'přiřadíme do druhého
w = v
'změníme druhý
w.X = 20
'co bude ve v.X?
Pokud je Vector struktura, bude tam pořád 15, ve w je kopie toho, co bylo v době přiřazení v proměnné v. Změnou w se ale první vektor v nezměnil.
Pokud Vector bude třída, proměnná obsahuje jen odkaz na blok paměti na haldě. Přiřazením v do w se zkopíruje jen ten odkaz, nic jiného. Obě proměnné nyní drží odkaz na stejné místo v paměti, změnou tohoto místa přes w se pochopitelně změna projeví i ve v. Hodnota v.X bude tedy 20. Je třeba si uvědomit, že nový objekt vzniká pouze voláním New, které je v ukázce kódu pouze jednou.
Pozor, kdybychom druhý vektor změnili třeba přes w = new Vector() With { X = 10, Y = 20 }, původní v se nezmění. Nezměnili jsme totiž objekt, ale pouze vytvořili nový a přiřadili jej do proměnné w. Referenci na první vektor jsme tedy akorát nahradili referencí na vektor druhý, ale samotný první vektor jsme nijak nezměnili.
Kontrolní otázka
Co se stane, když bude uvnitř datového typu Vector ještě pole integerů P? Pole je typ referenční, tím pádem pokud Vector bude struktura, bude se její paměťový prostor skládat ze dvou hodnot typu Double a jedné reference, samotné pole bude mimo datovou oblast struktury, bude ležet na haldě. Struktura se tedy zkopíruje - proměnné X a Y vzniknou nové a do proměnné P se nakopíruje reference z původní struktury. Takže budeme mít dvě struktury, každá bude mít svou proměnnou X a Y, ale obsah pole P zůstane stejný. Pokud přes v sáhneme do pole P a něco tam změníme, změní se to i v proměnné w.
Takhle to funguje třeba i v případě, kdy je ve struktuře String. Tam se nám ale změnou stringu v jedné struktuře změna jinde neprojeví. Proč? String je tzv. immutable datový typ, což znamená něco jako “nezměnitelný”. Vnitřní hodnotu proměnné typu string nikdy nemůžeme změnit. Pole znaků, které obsahuje, je read-only, a metody Trim, Substring apod. proměnnou, na které je voláme, nemění. Vytvoří prostě nový řetězec a ten vrátí. U Stringu se nám tedy nic podobného stát nemůže, objekt jako takový se nedá změnit. Jediné, co můžeme udělat, je vyrobit nový a přiřadit ho tam. Tím pádem si ale už každá struktura bude držet referenci na jiný string.
Jak klonovat objekty a struktury?
Občas se nám hodí umět vytvořit kopii objektu nebo struktury. U struktury už víme, že by mělo stačit vytvořit druhou proměnnou a přiřadit, ale to nebude fungovat správně, pokud máme uvnitř struktury referenční typy. U Stringu to ještě nevadí, ale u pole nebo u nějakého objektu už třeba ano. Jestli chceme kopírovat i pole nebo objekty, to už může záležet na logice naší aplikace, někdy to je žádoucí, jindy ne.
Abychom využívali standardní řešení .NET Frameworku, dělá se to tak, že třídě / struktuře naimplementujeme rozhraní ICloneable. Uvnitř metody Clone pak řekneme, jak se klonování má přesně provést. Hodí se ještě znát metodu MemberwiseClone, která umí udělat tzv. mělkou kopii. To je přesně to, co dělá přiřazování struktur - vezme datovou oblast struktury, která obsahuje všechny proměnné, a zkopíruje je. Metoda MemberwiseClone, která je definovaná v typu Object, umí to samé udělat s třídou.
Jak tedy zklonovat třídu Vector tak, aby se vytvořila opravdová kopie? Aby vznikly vlastní proměnné X a Y a celé vlastní pole P? Naimplementujeme ICloneable a metodu Clone.
Class Vector
Implements ICloneable
'proměnné
Public X As Double
Public Y As Double
Public P() As Integer
''' <summary>
''' Naklonuje objekt
''' </summary>
Public Function Clone() As Object Implements System.ICloneable.Clone
Dim copy As Vector
' udělat mělkou kopii (vytvoří se nový Vector a zkopírují se hodnoty vnitřních proměnných)
copy = Me.MemberwiseClone()
' zduplikovat pole, aby každá instance měla své vlastní
If Me.P IsNot Nothing Then
copy.P = Me.P.Clone()
End If
Return copy
End Function
End Class
Místo řádku copy = Me.MemberwiseClone() bychom klidně mohli napsat toto, je to naprosto to samé. Vytvoří se nový objekt a zkopírují se hodnoty proměnných. MemberwiseClone bude asi rychlejší, protože zkopíruje celý blok paměti najednou a nebude to dělat po proměnných. Ale co do funkčnosti je to naprosto to samé.
copy = New Vector()
copy.X = Me.X
copy.Y = Me.Y
copy.P = Me.P
No a jak objekt zklonujeme? Jednoduše:
'vytvořit první
Dim v As New Vector()
v.X = 15
v.Y = 20
v.P = New Integer() {15, 16, 17}
'klonovat
Dim w As Vector
w = v.Clone()
Závěrem
Při práci s datovými typy v .NET Frameworku je potřeba přemýšlet a uvědomovat si souvislosti. Pokud se ale naučíte základní pravidla a rozdíly mezi hodnotovými a referenčními typy, pak už je logické a odvoditelné, jak se to bude chovat.