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

4. diel - Referenčnej a hodnotové dátové typy vo Swift

V predchádzajúcom cvičení, Riešené úlohy k 3. lekcii OOP vo Swift, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.

V minulej lekcii, Riešené úlohy k 3. lekcii OOP vo Swift , sme si vytvorili svoj prvý poriadny objekt, bola ním hracia kocka. Začíname pracovať s objektmi a objekty sú referenčnými dátovými typy, ktoré sa v niektorých ohľadoch správajú inak, než typy hodnotové (napr. Int). Je dôležité, aby sme presne vedeli, čo sa vo vnútri programu deje, inak by nás v budúcnosti mohlo všeličo prekvapiť.

Zopakujme si pre istotu ešte raz, čo sú to hodnotovej typy. Všeobecne sú to jednoduché štruktúry, napr. Jedno číslo, jeden znak. Väčšinou sa chce, aby sme s nimi pracovali čo najrýchlejšie, v programe sa ich vyskytuje veľmi veľa a zaberajú málo miesta. V anglickej literatúre sú často opisované slovami light-weight. Majú pevnú veľkosť. Príkladom sú napr. Int, Float, Double, Character, Bool a ďalšie.

Aplikácia (resp. Jej vlákno) má operačným systémom pridelenú pamäť v podobe tzv. Zásobníka (stack). Jedná sa o veľmi rýchlu pamäť s priamym prístupom, jej veľkosť aplikácie nemôže ovplyvniť, prostriedky sú prideľované operačným systémom. Táto malá a rýchla pamäť je využívaná na ukladanie lokálnych premenných hodnotového typu (až na výnimky pri opakovaniach, ktorými sa nebudeme zaoberať). Premennou si v nej môžeme predstaviť asi takto:

Zásobník vb pamäte počítača - Objektovo orientované programovanie vo Swift

Na obrázku je znázornená pamäť, ktorú môže naše aplikácie využívať. V aplikácii sme si vytvorili premennú a typu Int. Jej hodnota je 56 a uložila sa nám priamo do zásobníka. Kód by mohol vyzerať takto:

let a : Int = 56

Môžeme to chápať tak, že premenná a má pridelenú časť pamäte v zásobníku (veľkosti dátového typu Int, teda 32 bitov), v ktorej je uložená hodnota 56.

Vytvorme si novú konzolovú aplikáciu a pridajme si k nej jednoduchú triedu, ktorá bude reprezentovať užívateľa nejakého systému. Pre názornosť vypustím komentáre a nebudem riešiť viditeľnosti:

class Uzivatel {
    var vek : Int
    var jmeno : String

    init(jmeno: String, vek: Int) {
        self.jmeno = jmeno
        self.vek = vek
    }
}

Trieda má 2 jednoduché verejné vlastnosti a konštruktor. Do nášho pôvodného programu pridajme vytvorení inštancie tejto triedy:

let a : Int = 56
let u = Uzivatel(jmeno: "Jan Novák", vek: 28)

Premenná u je teraz referenčného typu. Pozrime sa na novú situáciu v pamäti:

Zásobník a halda v pamäti počítača - Objektovo orientované programovanie vo Swift

Vidíme, že objekt (premenná referenčného dátového typu) sa už neukladá do zásobníka, ale do pamäte zvanej halda. Je to z toho dôvodu, že objekt je spravidla zložitejšie ako hodnotový dátový typ (väčšinou obsahuje hneď niekoľko ďalších vlastností) a tiež zaberá viac miesta v pamäti.

Zásobník aj halda sa nachádzajú v pamäti RAM. Rozdiel je v prístupe a veľkosti. Halda je prakticky neobmedzená pamäť, ku ktorej je však prístup zložitejšia a tým pádom pomalší. Naopak zásobník je pamäť rýchla, ale veľkostne obmedzená.

Premenné referenčného typu sú v pamäti uložené vlastne na dvakrát, raz v zásobníku a raz v halde. V zásobníku je uložená iba tzv. Referencie, teda odkaz do haldy, kde sa potom nachádza naozajstný objekt.

Napr. v C ++ je veľký rozdiel medzi pojmom ukazovateľ a referencie. Swift používa termín referencie, tie sa paradoxne princípom podobajú skôr ukazovateľom v C ++. Pojmy ukazovateľ a referencie tu spomenuté teda znamenajú referenciu v zmysle Swift a nemajú s C ++ nič spoločné.

Môžete sa pýtať, prečo je to takto urobené. Dôvodov je hneď niekoľko, poďme si niektoré vymenovať:

  1. Miesto vo stacku je obmedzené.
  2. Keď budeme chcieť použiť objekt viackrát (napr. Ho odovzdať ako parameter do niekoľkých metód), nemusíme ho v programe odovzdávať ako kópiu. Odovzdáme iba malý hodnotový typ s referenciou na objekt namiesto toho, aby sme všeobecne pamäťovo náročný objekt kopírovali. Toto si vzápätí ukážeme.
  3. Pomocou referencií môžeme jednoducho vytvárať štruktúry s dynamickou veľkosťou, napr. Štruktúry podobné poli, do ktorých môžeme za behu vkladať nové prvky. Tie sú na seba navzájom odkazované referenciami, ako reťaz objektov.

Založme si 2 premenné typu Int a 2 premenné typu Uzivatel:

var a : Int = 56
var b : Int = 28
var u = Uzivatel(jmeno: "Jan Novák", vek: 28)
var v = Uzivatel(jmeno: "Josef Nový", vek: 32)

Situácia v pamäti bude nasledovné:

Referenčné hodnoty vo Swift v pamäti počítača - Objektovo orientované programovanie vo Swift

Teraz skúsme priradiť do premennej a premennú b. Rovnako tak priradíme aj premennú v do premennej u. Hodnotový typ sa v zásobníku len skopíruje, pri objekte sa skopíruje iba referencie (čo je vlastne tiež hodnotový typ), ale objekt máme stále len jeden. V kóde vykonáme teda toto:

var a : Int = 56
var b : Int = 28
var u = Uzivatel(jmeno: "Jan Novák", vek: 28)
var v = Uzivatel(jmeno: "Josef Nový", vek: 32)
a = b
u = v

V pamäti bude celá situácia vyzerať nasledovne:

Referenčné hodnoty vo Swift v pamäti počítača - Objektovo orientované programovanie vo Swift

Presvedčte sa o tom, aby ste videli, že to naozaj tak je :) Najprv si necháme všetky štyri premenné vypísať pred a po zmene. Pretože budeme výpis volať viackrát, napíšem ho trochu úspornejšie. Upravme teda kód na nasledujúce:

// založení proměnných
var a : Int = 56
var b : Int = 28
var u = Uzivatel(jmeno: "Jan Novák", vek: 28)
var v = Uzivatel(jmeno: "Josef Nový", vek: 32)
print("a: \(a) \nb: \(b)\nu: \(u.jmeno) \nv: \(v.jmeno)\n")

// přiřazování
a = b
u = v
print("a: \(a) \nb: \(b)\nu: \(u.jmeno) \nv: \(v.jmeno)\n")
class Uzivatel {
    var vek : Int
    var jmeno : String

    init(jmeno: String, vek: Int) {
        self.jmeno = jmeno
        self.vek = vek
    }
}

\ ---

Na výstupe programu zatiaľ rozdiel medzi hodnotovým a referenčným typom nespoznáme:

a: 56
b: 28
u: Jan Novák
v: Josef Nový

a: 28
b: 28
u: Josef Nový
v: Josef Nový

Avšak vieme, že kým v a a b sú naozaj 2 rôzne čísla s rovnakou hodnotou, v u a v je ten istý objekt. Poďme zmeniť meno používateľa v a podľa našich predpokladov by sa mala zmena prejaviť aj v premennej u. K programu pripíšeme:

// změna
v.jmeno = "John Doe";
print("u: \(u.jmeno) \nv: \(v.jmeno)\n")
class Uzivatel {
    var vek : Int
    var jmeno : String

    init(jmeno: String, vek: Int) {
        self.jmeno = jmeno
        self.vek = vek
    }
}

Zmenili sme objekt v premennej v a znovu vypíšeme u a v:

a: 56
b: 28
u: Jan Novák
v: Josef Nový

a: 28
b: 28
u: Josef Nový
v: Josef Nový

u: John Doe
v: John Doe

Spolu so zmenou v sa zmení aj u, pretože premenné ukazujú na ten istý objekt. Ak sa pýtate, ako vytvoriť naozajstnú kópiu objektu, tak najjednoduchšie je objekt znova vytvoriť pomocou konstruktoru a dať do neho rovnaké dáta. Ďalej môžeme použiť klonovanie, ale o tom zas až niekedy inokedy. Pripomeňme si situáciu v pamäti ešte raz a zamerajme sa na Jána Nováka.

Referenčné hodnoty vo Swift v pamäti počítača - Objektovo orientované programovanie vo Swift

Čo sa sním stane? "Zožerie" ho tzv. ARC (Automatic Reference Counting). Obrázok nižšie je ilustratívny, pojem Garbage Collector je obdoba ARC u ostatných jazykov.

ARC vo Swift - Objektovo orientované programovanie vo Swift

Arc a dynamická správa pamäte

Pamäť môžeme v programoch alokovať staticky, to znamená, že v zdrojovom kóde vopred určíme, koľko jej budeme používať. Doteraz sme to tak vlastne robili a nemali sme s tým problém, pekne sme do zdrojového kódu napísali potrebné premenné. Čoskoro sa ale budeme stretávať s aplikáciami (a už sme sa vlastne i stretli), kedy nebudeme pred spustením presne vedieť, koľko pamäte budeme potrebovať.

V minulosti, hlavne v časoch jazykov C, Pascal a C ++, sa na tento účel používali tzv. Pointer, čiže priame ukazovatele do pamäte. Napospol to fungovalo tak, že sme si povedali operačnému systému o kus pamäti o určitej veľkosti. On ju pre nás vyhradil a dal nám jej adresu. Na toto miesto v pamäti sme mali pointer, cez ktorý sme s pamäťou pracovali. Problém bol, že nikto nestrážil, čo do pamäti dávame (ukazovateľ smeroval na začiatok vyhradeného priestoru). Keď sme tam dali niečo väčšieho, skrátka sa to rovnako uložilo a prepísala sa dáta za naším priestorom, ktorá patrila napríklad inému programu alebo operačnému systému (v tom prípade by našu aplikáciu OS asi zabil - zastavil). Často sme si však my v pamäti prepísali nejaká ďalšie dáta nášho programu a program sa začal správať chaoticky. Predstavte si, že si uložíte používateľa do poľa a v tej chvíli sa vám zrazu zmení farba užívateľského prostredia, teda niečo, čo s tým vôbec nesúvisí. Hodiny strávite tým, že kontrolujete kód pre zmenu farby, potom zistíte, že je chyba v založenia používateľa, kedy dôjde k pretečeniu pamäte a prepísanie hodnôt farby.

Keď naopak nejaký objekt prestaneme používať, musíme po ňom miesto sami uvoľniť, ak to neurobíme, pamäť zostane blokovaná. Pokiaľ toto robíme napr. V nejakej metóde a zabudneme pamäť uvoľňovať, naše aplikácie začne padať, prípadne zasekne celý operačný systém. Takáto chyba sa opäť zle hľadá, prečo program prestane po niekoľkých hodinách fungovať? Kde tú chybu v niekoľkých tisícoch riadkov kódu vôbec hľadať? Nemáme jedinú stopu, nemôžeme sa ničoho chytiť, musíme prejsť celý program riadok po riadku alebo začať preskúmavať pamäť počítača, ktorá je v binárce. Brrr. Podobný problém nastane, keď si niekde pamäť uvoľníme a následne pointer opäť použijeme (zabudneme, že je uvoľnený, to sa môže ľahko stať), povedie niekam, kde je už uloženého niečo iné a tieto dáta budú opäť prepísané. Povedie to k nekontrolovanému správanie našej aplikácie a môže to dopadnúť aj takto:

Blue Screen Of Death – BSOD vo Windows - Objektovo orientované programovanie vo Swift

ARC je súčasťou priamo aplikačného kódu a všetko potrebné do našej aplikácie vloží kompilátor pri preklade našej aplikácie. Už vieme, že skratka znamená Automatic Reference Counting, teda automatické počítanie referencií. Akonáhle sa počet referencií na nejaký objekt dostane na nulu, dôjde k delokácia a teda odstránenie tohto objektu z pamäte. Program sa totiž dostal do fázy, kedy vie, že sa k objektu nedá dostať a teda zbytočne zaberá miesto. Neskôr sa dostaneme ešte ku kľúčovému slovu weak, ktorým je možné označiť premenné. Weak referencie sa nepočítajú do "ARC počítadla" a akonáhle existujú iba weak referencie, dochádza k delokácia, pretože tieto weak referencie nebráni ARC, aby daný objekt dealokoval. Toto je nutné riešiť v prípade, keď dve inštancie na seba drží referencie a dôjde k tzv. "Zacykleniu" a nikdy nedôjde k uvoľneniu pamäti, pretože na oba objekty stále niečo odkazuje (v tomto prípade vždy ten druhý objekt).

Hodnota nil

Posledná vec, o ktorej sa zmienime (respektíve si pripomenieme), je tzv. Hodnota nil, ktorú už poznáte zo základného kurzu o type Optional. Na rozdiel od ďalších jazykov, kde existuje ekvivalent v podobe null, nemôže vo Swift len tak nejaká premenná nadobúdať hodnoty nil, musí byť označená ako Optional.

Keď nastavíme nejakú premennú na nil, zrušíme iba tú jednu referenciu. Pokiaľ na náš objekt existuje ešte nejaká referencie, bude aj naďalej existovať. Ak nie, bude uvoľnený ARC.

K referenčným typom sa ešte raz vrátime. V budúcej lekcii, Riešené úlohy k 4. lekcii OOP vo Swift , si zas niečo praktické naprogramujeme, nech si vedomosti zažijeme. Prezradím, že pôjde o objekt bojovníka do našej arény. To je zatiaľ všetko :)

V nasledujúcom cvičení, Riešené úlohy k 4. lekcii OOP vo Swift, si precvičíme nadobudnuté skúsenosti z predchádzajúcich lekcií.


 

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 7x (16.21 kB)

 

Predchádzajúci článok
Riešené úlohy k 3. lekcii OOP vo Swift
Všetky články v sekcii
Objektovo orientované programovanie vo Swift
Preskočiť článok
(neodporúčame)
Riešené úlohy k 4. lekcii OOP vo Swift
Článok pre vás napísal Filip Němeček
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje vývoji iOS aplikací (občas macOS)
Aktivity