IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

Immutable objects (nemenné objekty)

U nemenných objektov nemôžeme zmeniť ich vnútorný stav. VC # .NET / Jave je to napríklad trieda String, ktorá po každej operácii vracia nový objekt typu String. Pozrieme sa na spôsob ako správne takéto objekty implementovať a vysvetlíme si k čomu nám sú dobré.

Motivácia

V programoch často pracujeme s jednoduchými triedami ako je Rect (obdĺžnik), Point (bod), Line (priamka), Interval a im podobné. Spoločnou charakteristikou týchto tried je, že majú relatívne jednoduché rozhranie (spravidla len niekoľko atribútov) a využívame je len lokálne. Málokedy potrebujeme, aby sa zmenila súradnice obdĺžnika niekde inde v programe a preto je vhodné zmenu priamo zakázať pre ďalšie zjednodušenie kódu. V niektorých prípadoch nemáme ani na vybranú, pretože nemennosť vyžaduje hardvér, operačný systém počítača alebo daná technológie (napr. Pri použití vlákien je oveľa jednoduchšie používať immutable objekty, pretože sú thread-safe).

Prečo je String nemenný?

Problematiku si vysvetlíme na triede String. Pre tých, čo programujú v C ++ je odpoveď zrejmá. Pre ostatné tu prinášam menšie objasnenie.

Textový reťazec je v počítači reprezentovaný ako súvislý blok pamäte. Z programátorského hľadiska by sme mohli povedať, že sa jedná o pole znakov. Na rozdiel od polí vo vyšších programovacích jazykoch (napr. PHP) majú polia v C / C ++ určité obmedzujúce vlastnosti, napríklad sa automaticky nezväčšujú. Keď teda v C vytvoríme poľa, jedná sa o prostý blok pamäte. Ak chceme pole zväčšiť, musíme požiadať o ďalšie (väčšie) miesto niekde inde v pamäti, pôvodnej hodnoty do neho skopírovať a potom pôvodný (menšie) pole uvoľniť. Pole nemožno jednoducho zväčšiť z toho dôvodu, že za ním v pamäti skrátka nemusia byť voľno a nedalo by sa predĺžiť.

Hoci môžeme v programe (i v C) použiť zoznamy, ktoré sa automaticky rozširujú (tzv. Spojové zoznamy), operačný systém si s nimi neporadí. Preto napr. Pred vypísaním textu do konzoly operačného systému (ktoré chceme pravdepodobne vykonávať pomerne často) musíme vytvoriť klasické polia a skopírovať do neho náš text. Aby bol jazyk efektívne, mala by trieda String ukladať reťazca tak, ako ich potrebuje operačný systém.

Je teda zrejmé, že u reťazcov (ktoré String reprezentuje) je nutné zakázať ich modifikáciu, pretože by po zmene mohol byť text inak dlhý a to implementácia neumožňuje (je nutné vytvorenie nového poľa a prekopírovaním poľa pôvodného, vo výsledku máme v pamäti dva reťazce, keď sa jeden potom uvoľní).

Pozn .: Niečo iné je v trieda StringBuilder, ktorá využíva práve polí alebo spojových zoznamov, aby mohla reťazec voľne modifikovať. Čo je ale dôležité, reťazec dostaneme až vo chvíli, keď zavoláme metódu toString (). Tá nerobí nič iné, než že prevedie zoznam na pole znakov, ktorý vráti.

Hodnotovej dátové typy

Prečo zmieňujem hodnotové dátové typy, teda základné dátový typy ako je int, double a ďalšie? Pretože keď sa na chvíľku zamyslíme, zistíme, že všetky hodnotové dátové typy sú nemenné. Uložíme Ak celé číslo, vieme, že ak sme mu nepriradili inú hodnotu, bude jeho hodnota stále rovnaká. A to bez ohľadu na to, koľkých funkciami prešiel. To u referenčných typov (objektov) nevieme, pretože každá z metód môže zmeniť vnútorný stav objektu.

Vo vyšších jazykoch (PHP alebo Java) môžeme však zvyčajne vytvárať len objektové typy. To je ďalší z vecí, kde sa líšia C / C ++ od vyšších programovacích jazykov. V nižších programovacích jazykoch si môžeme povedať, akým spôsobom chceme premenou (alebo objekt) odovzdať. Ak funkcia dostane referenciu alebo kópiu samotného objektu si špecifikuje sám autor metódy. Vo vyšších programovacích jazykoch sú objekty vždy odovzdávané ako referencie.

Našou úlohou teraz bude definovať triedu, ktorá bude odovzdávaná referencií tak, aby nebolo možné jej vnútornú hodnotu zmeniť a svojím spôsobom sa správala ako hodnotový dátový typ.

Implementácia

Implementácie je veľmi kľúčová, ak niekde necháme medzierku, môže nám program premennú zmeniť. Z hľadiska návrhu si budeme musieť dať pozor na niekoľko veci:

  • Všetky atribúty, na ktorých je trieda závislá, definujeme ako konštantná, poprípade read-only alebo iným spôsobom (záleží na jazyku), aby nám kompilátor kontroloval či sa hodnota mení.
  • Dáme si pozor na všetky atribúty, ktoré sú referenčnými typy (priraďujeme im objekty). Vždy musíme urobiť hlbokú kópiu objektu, aby k nej žiadna iná časť programu nemohla pristupovať. Keby sme to neurobili, nezmeníme síce priamo hodnotu nemenného typu, ale zmeníme nepriamo jeho vnútornú reprezentáciu. Viac informácií o hlboké kópiu nájdete v tunajších kurzoch základov OOP u konkrétneho jazyka, pod pojmom "klonovanie".
  • Väčšinou musíme prepísať ďalšie, predvolené metódy objektov. Napr. v C# .NET sú to metódy GetHashCode () a Equals () a v Jave hashCode () a equals (). Predvolené implementácia metódy GetHashCode () / hashCode () sa rozhoduje na základe umiestnenia v pamäti. Teda ak porovnávame dva objekty (alebo chceme získať ich hash) vráti sa rovnaká hodnota pre objekty, ktoré sú v rovnakej pamäti (prakticky to znamená, že sa jedná o identické objekty - porovnávame objekt sám so sebou). Naše implementácie sa musí rozhodovať na základe hodnôt, ktoré nemenný objekt má, a nie na základe umiestnenia v pamäti.
  • Všetky funkcie, ktoré nejakým spôsobom operujú s vnútorným stavom objektu, nesmie meniť hodnoty objektu priamo, ale musí vracať novú inštanciu so zmenenými hodnotami. Napríklad pre celé číslo by operácie + - * /% vracali vždy novú inštanciu. Pôvodný inštancie by zostala nezmenená. Všimnime si, že je to presne správanie, ktoré od celočíselných typov očakávame.

Časté chyby

  • Osobitnú pozor si musíme dať na kopírovacie konštruktor a metódu Clone () (alebo ktorejkoľvek metódu vytvárajúca kópiu). Kópia musí byť vždy hlboká. Na obdobnom princípe funguje kopírovacie konštruktor, ktoré si musí vytvoriť kópiu pôvodného objektu a až potom môže hodnoty priradiť k vlastným atribútom.
  • Operátor = je trochu kontroverzný. V niektorých jazykoch si ho môžeme sami nadefinovať, v niektorých to nie je možné (vyššia jazyky). Asi cítime, že operátor = by mal zmeniť stav objektu, môžeme ho teda použiť s nemennými objektmi? Odpoveď je áno, pretože operátor = nemení hodnoty pôvodného objektu, ale len prepíše referenciu, aby sa odvolával na iný objekt.

Teraz sa už môžeme pozrieť na implementáciu takého nemenného objektu v C# .NET. Budeme predpokladať, že neexistuje typ pre komplexné čísla. My si taký typ vytvorí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);
    }

}
Nemenné objekty – diagram - Návrhové vzory

Výhody

Nemenné objekty nám poskytujú niekoľko výhod, ktorých môžeme využiť.

  • Hodnoty sa nám nemení pod rukami. Máme istotu, že čo sme do premennej uložili tam bude na ľubovoľnom inom mieste v programe.
  • S predchádzajúcim bodom súvisí i bezpečnosť z hľadiska viacerých vlákien. Pretože v žiadnej z vlákien nemôže premennú zmeniť, len vytvoriť jej kópiu, máme zaistené, že nemôže dôjsť ku kolízii pri zapisovaní niekoľkých vláknami. Vo veľkých aplikáciách si s týmito typmi objektov ušetríme veľa zamykanie, zbytočných chýb a vývojového času.

Zmena vnútorného stavu

Časté vytváranie a mazanie objektov z pamäte môže mať neblahý dôsledok na výkon programu. Preto väčšina nemenných typov prichádza s ich měnnými variantmi. Príklad môže byť práve trieda String a StringBuilder. Hoci je v globálnom meradle práce sa StringBuilderem pomalší ako práca so samotným String, za časté zmeny neplatíme žiadnu réžii. To ale neznamená, že by mohol StringBuilder nahradiť String. StringBuilder (ako už názov napovedá) slúži na vytváranie a modifikáciu reťazca. Kedykoľvek môžeme zavolať metódu toString () a získať reťazec, s ktorým budeme ďalej pracovať. A ako už bolo povedané na začiatku, väčšina knižníc potrebuje reťazec vo formáte ako ho ukladá String. V takýchto situáciách nemôžeme do metódy odovzdať StringBuilder.


 

Všetky články v sekcii
Návrhové vzory
Článok pre vás napísal Patrik Valkovič
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity