.NET Tip #32: Jak funguje operátor přiřazení a jak klonovat objekty

Tomáš Herceg       29. 7. 2009       C#, VB.NET, .NET Tips       7661 zobrazení

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.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Diskuse: .NET Tip #32: Jak funguje operátor přiřazení a jak klonovat objekty

Ahoj, jmenuju se Jirka a mám dotaz. Není to náhodou při kopírování struktur se stringem tak, že v kopii struktury se nachází odkaz na původní string a až s novým řetězcem v kopii se vytvoří nový odkaz na objekt string ? Tzn., že všechny kopie struktury dokud nedojde ke změně obsahují odkaz na původní string ?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Ano - obě kopie struktury budou obsahovat odkaz na původní string. To však vůbec nevadí, protože string je immutable a nedá se změnit. Jediná možnost, jak ve struktuře string změnit, je přiřadit tam jiný. Pak se ale tato změna ve druhé struktuře neprovede, reference povede stále na původní řetězec.

Chová se to tedy intuitivně tak, jak by člověk čekal.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Diskuse: .NET Tip #32: Jak funguje operátor přiřazení a jak klonovat objekty

Dobrý den,

děkuji za pěkný článek. Určitě by bylo fajn vytvořit druhý díl, ve kterém byste mohl poukázat na možnosti (a nemožnosti) deep klonování objektů serializací a reflexí.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Deep klonování objektů si můžete udělat sám právě přes ICloneable. Je již na vás, jakou logiku zachováte.

O serializaci by se dal udělat celý seriál, časem se k němu možná také dostanu.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Diskuse: .NET Tip #32: Jak funguje operátor přiřazení a jak klonovat objekty

Vím, že je to nepodstatné, ale jaksi jsem, Tomáši, nepochytil Vaše "chytáky".

Poté, co jsem vzdal přemýšlení o tom, proč je zrovna dvaatřivátý .NET Tip považován za jubilejní jsem započal bloumat nad tím, kterou ruku mám levou a kterou pravou. A i zde jsem nepochodil (asi to bude tím horkem). Přiznám se, že do přečtení Vašeho článku jsem byl vždy přesvědčen, že u operátoru přiřazení je výraz na jeho pravé straně a proměnná, kam se přiřazuje na jeho straně levé, ještě že jste mne vyvedl z mnohaletého omylu.

Poslední, na co jsem dost dlouho zíral, byl ten Váš příklad třídy Vector. Konkrétně jsem v něm hledal (marně) řádek copy = Me.MemberwiseClone(), k jehož nahrazení mne nabádáte hned pod ukázkou kódu. Asi mám dneska nějaký slabší den :-(

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

32 je ve dvojkové soustavě 100000, což jubilejní číslo určitě je. Za prohození pravé a levé strany se omlouvám, odjakživa si je pletu a dnes nemám svůj den. A ten řádek s MemberwiseClone mi nějak vypadl, za což se též omlouvám. Až budu doma, tak to opravím.

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback