Předvánoční slevová akce Předvánoční slevová akce
Využij předvánočních slev a získej od nás 20 % bodů zdarma! Více zde

Immutable objects (neměnné objekty)

Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem.
Vydávání, hosting a aktualizace umožňují jeho sponzoři.

U neměnných objektů nemůžeme změnit jejich vnitřní stav. V C# .NET/Javě je to například třída String, která po každé operaci vrací nový objekt typu String. Podíváme se na způsob jak správně takové objekty implementovat a vysvětlíme si k čemu nám jsou dobré.

Motivace

V programech často pracujeme s jednoduchými třídami jako je Rect (obdélník), Point (bod), Line (přímka), Interval a jim podobné. Společnou charakteristikou těchto tříd je, že mají relativně jednoduché rozhraní (zpravidla pouze několik atributů) a využíváme je pouze lokálně. Málokdy potřebujeme, aby se změnila souřadnice obdélníku někde jinde v programu a proto je vhodné změnu přímo zakázat pro další zjednodušení kódu. V některých případech nemáme ani na vybranou, jelikož neměnnost vyžaduje hardware, operační systém počítače nebo daná technologie (např. při použití vláken je mnohem jednodušší používat immutable objekty, jelikož jsou thread-safe).

Proč je String neměnný?

Problematiku si vysvětleme na třídě String. Pro ty, co programují v C++ je odpověď zřejmá. Pro ostatní zde přináším menší objasnění.

Textový řetězec je v počítači reprezentován jako souvislý blok paměti. Z programátorského hlediska bychom mohli říci, že se jedná o pole znaků. Na rozdíl od polí ve vyšších programovacích jazycích (např. PHP) mají pole v C/C++ určité omezující vlastnosti, například se automaticky nezvětšují. Když tedy v C vytvoříme pole, jedná se o prostý blok paměti. Pokud chceme pole zvětšit, musíme požádat o další (větší) místo někde jinde v paměti, původní hodnoty do něj zkopírovat a poté původní (menší) pole uvolnit. Pole nelze jednoduše zvětšit z toho důvodu, že za ním v paměti zkrátka nemusí být volno a nedalo by se prodloužit.

Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!

Ačkoliv můžeme v programu (i v C) použít seznamy, které se automaticky rozšiřují (tzv. spojové seznamy), operační systém si s nimi neporadí. Proto např. před vypsáním textu do konzole operačního systému (které chceme pravděpodobně provádět poměrně často) musíme vytvořit klasické pole a zkopírovat do něj náš text. Aby byl jazyk efektivní, měla by třída String ukládat řetězce tak, jak je potřebuje operační systém.

Je tedy patrné, že u řetězců (které String reprezentuje) je nutné zakázat jejich modifikaci, protože by po změně mohl být text jinak dlouhý a to implementace neumožňuje (je nutné vytvoření nového pole a překopírováním pole původního, ve výsledku máme v paměti dva řetězce, kdy se jeden poté uvolní).

Pozn.: Něco jiného je v třída StringBuilder, která využívá právě polí nebo spojových seznamů, aby mohla řetězec volně modifikovat. Co je ale důležité, řetězec dostaneme až ve chvíli, kdy zavoláme metodu ToString(). Ta nedělá nic jiného, než že převede seznam na pole znaků, který vrátí.

Hodnotové datové typy

Proč zmiňuji hodnotové datové typy, tedy základní datový typy jako je int, double a další? Protože když se na chvilku zamyslíme, zjistíme, že všechny hodnotové datové typy jsou neměnné. Uložíme-li celé číslo, víme, že pokud jsme mu nepřiřadili jinou hodnotu, bude jeho hodnota stále stejná. A to bez ohledu na to, kolika funkcemi prošel. To u referenčních typů (objektů) nevíme, protože každá z metod může změnit vnitřní stav objektu.

Ve vyšších jazycích (PHP nebo Java) můžeme však obvykle vytvářet jen objektové typy. To je další z věcí, kde se liší C/C++ od vyšších programovacích jazyků. V nižších programovacích jazycích si můžeme říci, jakým způsobem chceme proměnou (nebo objekt) předat. Jestli funkce dostane referenci nebo kopii samotného objektu si specifikuje sám autor metody. Ve vyšších programovacích jazycích jsou objekty vždy předávány jako reference.

Naším úkolem nyní bude definovat třídu, která bude předávána referencí tak, aby nebylo možné její vnitřní hodnotu změnit a svým způsobem se chovala jako hodnotový datový typ.

Implementace

Implementace je velmi klíčová, pokud někde necháme skulinku, může nám program proměnnou změnit. Z hlediska návrhu si budeme muset dát pozor na několik věci:

  • Všechny atributy, na kterých je třída závislá, definujeme jako konstantní, popřípadě read-only nebo jiným způsobem (záleží na jazyku), aby nám kompilátor kontroloval jestli se hodnota mění.
  • Dáme si pozor na všechny atributy, které jsou referenčními typy (přiřazujeme jim objekty). Vždy musíme udělat hlubokou kopii objektu, aby k ní žádná jiná část programu nemohla přistupovat. Kdybychom to neprovedli, nezměníme sice přímo hodnotu neměnného typu, ale změníme nepřímo jeho vnitřní reprezentaci. Více informací o hluboké kopii naleznete ve zdejších kurzech základů OOP u konkrétního jazyka, pod pojmem "klonování".
  • Většinou musíme přepsat další, výchozí metody objektů. Např. v C# .NET jsou to metody GetHashCode() a Equals() a v Javě hashCode() a equals(). Výchozí implementace metody GetHashCode()/hashCo­de() se rozhoduje na základě umístění v paměti. Tedy jestliže porovnáváme dva objekty (nebo chceme získat jejich hash) vrátí se stejná hodnota pro objekty, které jsou ve stejné paměti (prakticky to znamená, že se jedná o identické objekty – porovnáváme objekt sám se sebou). Naše implementace se musí rozhodovat na základě hodnot, které neměnný objekt má, a nikoliv na základě umístění v paměti.
  • Všechny funkce, které nějakým způsobem operují s vnitřním stavem objektu, nesmí měnit hodnoty objektu přímo, ale musí vracet novou instanci se změněnými hodnotami. Například pro celé číslo by operace +-*/% vracely vždy novou instanci. Původní instance by zůstala nezměněna. Všimněme si, že je to přesně chování, které od celočíselných typů očekáváme.

Časté chyby

  • Zvláštní pozor si musíme dát na kopírovací konstruktor a metodu Clone() (či jakoukoliv metodu vytvářející kopii). Kopie musí být vždy hluboká. Na obdobném principu funguje kopírovací konstruktor, které si musí vytvořit kopii původního objektu a teprve poté může hodnoty přiřadit k vlastním atributům.
  • Operátor = je poněkud kontroverzní. V některých jazycích si jej můžeme sami nadefinovat, v některých to není možné (vyšší jazyky). Asi cítíme, že operátor = by měl změnit stav objektu, můžeme jej tedy použít s neměnnými objekty? Odpověď je ano, protože operátor = nemění hodnoty původního objektu, ale jen přepíše referenci, aby se odkazoval na jiný objekt.

Nyní se již můžeme podívat na implementaci takového neměnného objektu v C# .NET. Budeme předpokládat, že neexistuje typ pro komplexní čísla. My si takový typ vytvoříme.

class KomplexniCislo : ICloneable
{
    private readonly int RealnaCast;
    private readonly int ImaginarniCast;

    public KomplexniCislo(int RealnaCast, int ImaginarniCast)
    {
        this.RealnaCast = RealnaCast;
        this.ImaginarniCast = ImaginarniCast;
    }

    public KomplexniCislo()
    {
        this.RealnaCast = 0;
        this.ImaginarniCast = 0;
    }

    public KomplexniCislo(KomplexniCislo Copy)
    {
        this.RealnaCast = Copy.RealnaCast;
        this.ImaginarniCast = Copy.ImaginarniCast;
    }

    public object Clone()
    {
        return new KomplexniCislo(RealnaCast, ImaginarniCast);
    }

    public override bool Equals(object DruheCislo)
    {
        if (!(DruheCislo is KomplexniCislo))
            return false;
        KomplexniCislo DruheKomplexniCislo = (KomplexniCislo)DruheCislo;
        return DruheKomplexniCislo.GetHashCode() == this.GetHashCode();
    }

    public override int GetHashCode()
    {
        return RealnaCast.GetHashCode() * 73 + ImaginarniCast.GetHashCode();
    }

    public static bool operator ==(KomplexniCislo prvni, KomplexniCislo druhe)
    {
        return prvni.Equals(druhe);
    }

    public static bool operator !=(KomplexniCislo prvni, KomplexniCislo druhe)
    {
        return !prvni.Equals(druhe);
    }

    public static KomplexniCislo operator +(KomplexniCislo prvni, KomplexniCislo druhe)
    {
        return new KomplexniCislo(prvni.RealnaCast+druhe.RealnaCast,prvni.ImaginarniCast+druhe.ImaginarniCast);
    }

}
Neměnné objekty – diagram

Výhody

Neměnné objekty nám poskytují několik výhod, kterých můžeme využít.

  • Hodnoty se nám nemění pod rukama. Máme jistotu, že co jsme do proměnné uložili tam bude na libovolném jiném místě v programu.
  • S předchozím bodem souvisí i bezpečnost z hlediska více vláken. Protože v žádné z vláken nemůže proměnnou změnit, pouze vytvořit její kopii, máme zajištěno, že nemůže dojít ke kolizi při zapisování několika vlákny. Ve velkých aplikacích si s těmito typy objektů ušetříme spoustu zamykání, zbytečných chyb a vývojového času.

Změna vnitřního stavu

Časté vytváření a mazání objektů z paměti může mít neblahý důsledek na výkon programu. Proto většina neměnných typů přichází s jejich měnnými variantami. Příklad může být právě třída String a StringBuilder. Ačkoliv je v globálním měřítku práce se StringBuilderem pomalejší než práce se samotným Stringem, za časté změny neplatíme žádnou režii. To ale neznamená, že by mohl StringBuilder nahradit String. StringBuilder (jak již název napovídá) slouží k vytváření a modifikaci řetězce. Kdykoliv můžeme zavolat metodu ToString() a získat řetězec, se kterým budeme dále pracovat. A jak již bylo řečeno na začátku, většina knihoven potřebuje řetězec ve formátu jak jej ukládá String. V takových situacích nemůžeme do metody předat StringBuilder.


 

 

Článek pro vás napsal Patrik Valkovič
Avatar
Jak se ti líbí článek?
6 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Předchozí článek
Object pool (fond objektů)
Všechny články v sekci
Návrhové vzory
Miniatura
Následující článek
Method chaining a method cascading
Aktivity (5)

 

 

Komentáře
Zobrazit starší komentáře (5)

Avatar
Lukáš Kún
Člen
Avatar
Lukáš Kún:11.9.2017 10:16

Jo jasne, sorry. Mluvil jsi totiz predtim o stringach v cecku a jejich reprezentaci v pameti, tak jsem tam zpocatku nevidel souvislost :)

 
Odpovědět
11.9.2017 10:16
Avatar
Petr Homola
Redaktor
Avatar
Petr Homola:21.10.2017 14:17

Ještě by to chtělo něco o objektech, jejichž stav se sice mění, ale jejich sémantika je imutabilní, ty jsou totiž pro vývoj nejlepší díky vyšší efektivitě. Možná v nějakém dalším článku?

 
Odpovědět
21.10.2017 14:17
Avatar
Patrik Pastor:17. června 20:27

1)
Mam mozna takovy blby dotaz, ale nemelo by byt v tom class diagramu u sipky "implements" misto extends? Prece jenom se extendujou classy a implenemtuji interfacy (tvuj pripad). A neni nahodou i klicove slovo v Jave pro implementovani interfacu "implements" a u implementovani tridy "extends"? (Znam zatim jenom C#, tam vse resi dvojtecka)

  1. Jak tady Homola zminil nejakou semantiku objektu, mohl by nekdo ve strucnosti rict o jakou semantiku jde? Jestli je myslena vnitrini struktura objektu nebo co presne? (znam semantiku css, kde se resi layout stranky semanticky, ale predpokladam, ze to nema s timto nic spolecneho).

Dik za odpovedi

 
Odpovědět
17. června 20:27
Avatar
Odpovídá na Patrik Pastor
Ondřej Štorc:17. června 20:38

Šipka ukazuje správně :) Koukni se na to jak fungují UML diagramy (třeba tady https://www.itnetwork.cz/…tridni-model).

Odpovědět
17. června 20:38
Život je příliš krátký na to, abychom bezpečně odebírali USB z počítače..
Avatar
Odpovídá na Ondřej Štorc
Patrik Pastor:17. června 20:56

a jak by byl potom napsany kod v Jave? taky by bylo: KomplexniCislo extends IClonable? Nebo by bylo Komplexni cislo implements IClonable? Protoze tady

https://stackoverflow.com/…e-difference

To typek presne vysvetluje jak sem to psal, ze implements je pro interfacy a extends je pro classy, tak kdo ma ted pravdu??

 
Odpovědět
17. června 20:56
Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!
Avatar
Patrik Valkovič
Šéfredaktor
Avatar
Odpovídá na Patrik Pastor
Patrik Valkovič:17. června 21:29

Ahoj. Tohle je UML diagram, a ten je nezávislý na programovacím jazyce. Extends prostě říká, že rozšiřuje funkcionalitu. O tom, jakým způsobem se z diagramu vygeneruje kód (zda použije extends, implements nebo jen dvojtečku) už se stará nástroj pro generování kódu, z hlediska UML je to irelevantní.

Odpovědět
17. června 21:29
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Avatar
Odpovídá na Patrik Valkovič
Patrik Pastor:17. června 21:32

To chapu, ja jsem jenom myslel ze je nejaka vazba mezi UML syntaxi a Javou (protoze, pokud se nepletu, UML vznikalo pro projekty vyuzivajici prave Javu tehdy, samozrejme ne jenom). Vim ze je UML jazykove a platformne nezavisle, jen sem se domnival, ze prave z te doby zustaly "pozustatky" ze syntaxi Javy, ktera byla na projekty vyuzivajici UML dominantni (velke projekty Banky, apod).

 
Odpovědět
17. června 21:32
Avatar
Odpovídá na Patrik Valkovič
Patrik Pastor:17. června 22:57

nebo se mylim? v cem?

 
Odpovědět
17. června 22:57
Avatar
Luboš Běhounek Satik
Autoredaktor
Avatar
Odpovídá na Patrik Pastor
Luboš Běhounek Satik:18. června 0:49

Ano, mělo by tam být implements a u šipky ta čára přerušovaná, v článku je to špatně. :)

Editováno 18. června 0:51
Odpovědět
18. června 0:49
https://www.facebook.com/peasantsandcastles/
Avatar
Odpovídá na Luboš Běhounek Satik
Patrik Pastor:18. června 7:35

dik, je to sice detail, ale prave proto, ze nemam Javu, tak sem si nebyl jisty.

 
Odpovědět
18. června 7:35
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 15. Zobrazit vše