IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 4 - Referenční a hodnotové datové typy ve Swift

V předešlém cvičení, Řešené úlohy k 3. lekci OOP ve Swift, jsme si procvičili nabyté zkušenosti z předchozích lekcí.

Začínáme pracovat s objekty a objekty jsou referenčními datovými typy, které se v některých ohledech chovají jinak, než typy hodnotové (např. Int). Je důležité, abychom přesně věděli, co se uvnitř programu děje, jinak by nás v budoucnu mohlo leccos překvapit.

Zopakujme si pro jistotu ještě jednou, co jsou to hodnotové typy. Obecně jsou to jednoduché struktury, např. jedno číslo, jeden znak. Většinou se chce, abychom s nimi pracovali co nejrychleji, v programu se jich vyskytuje velmi mnoho a zabírají málo místa. V anglické literatuře jsou často popisovány slovy light-weight. Mají pevnou velikost. Příkladem jsou např. Int, Float, Double, Character, Bool a další.

Aplikace (resp. její vlákno) má operačním systémem přidělenou paměť v podobě tzv. zásobníku (stack). Jedná se o velmi rychlou paměť s přímým přístupem, její velikost aplikace nemůže ovlivnit, prostředky jsou přidělovány operačním systémem. Tato malá a rychlá paměť je využívána k ukládání lokálních proměnných hodnotového typu (až na výjimky při iteracích, kterými se nebudeme zabývat). Proměnnou si v ní můžeme představit asi takto:

Zásobník vb paměti počítače - Objektově orientované programování ve Swift

Na obrázku je znázorněna paměť, kterou může naše aplikace využívat. V aplikaci jsme si vytvořili proměnnou a typu Int. Její hodnota je 56 a uložila se nám přímo do zásobníku. Kód by mohl vypadat takto:

let a : Int = 56

Můžeme to chápat tak, že proměnná a má přidělenu část paměti v zásobníku (velikosti datového typu Int, tedy 32 bitů), ve které je uložena hodnota 56.

Vytvořme si novou konzolovou aplikaci a přidejme si k ní jednoduchou třídu, která bude reprezentovat uživatele nějakého systému. Pro názornost vypustím komentáře a nebudu řešit viditelnosti:

class Uzivatel {
    var vek : Int
    var jmeno : String

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

Třída má 2 jednoduché veřejné vlastnosti a konstruktor. Do našeho původního programu přidejme vytvoření instance této třídy:

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

Proměnná u je nyní referenčního typu. Podívejme se na novou situaci v paměti:

Zásobník a halda v paměti počítače - Objektově orientované programování ve Swift

Vidíme, že objekt (proměnná referenčního datového typu) se již neukládá do zásobníku, ale do paměti zvané halda. Je to z toho důvodu, že objekt je zpravidla složitější než hodnotový datový typ (většinou obsahuje hned několik dalších vlastností) a také zabírá více místa v paměti.

Zásobník i halda se nacházejí v paměti RAM. Rozdíl je v přístupu a velikosti. Halda je prakticky neomezená paměť, ke které je však přístup složitější a tím pádem pomalejší. Naopak zásobník je paměť rychlá, ale velikostně omezená.

Proměnné referenčního typu jsou v paměti uloženy vlastně nadvakrát, jednou v zásobníku a jednou v haldě. V zásobníku je uložena pouze tzv. reference, tedy odkaz do haldy, kde se poté nalézá opravdový objekt.

Např. v C++ je velký rozdíl mezi pojmem ukazatel a reference. Swift používá termín reference, ty se paradoxně principem podobají spíše ukazatelům v C++. Pojmy ukazatel a reference zde zmíněné tedy znamenají referenci ve smyslu Swift a nemají s C++ nic společného.

Můžete se ptát, proč je to takto udělané. Důvodů je hned několik, pojďme si některé vyjmenovat:

  1. Místo ve stacku je omezené.
  2. Když budeme chtít použít objekt vícekrát (např. ho předat jako parametr do několika metod), nemusíme ho v programu předávat jako kopii. Předáme pouze malý hodnotový typ s referencí na objekt místo toho, abychom obecně paměťově náročný objekt kopírovali. Toto si vzápětí ukážeme.
  3. Pomocí referencí můžeme jednoduše vytvářet struktury s dynamickou velikostí, např. struktury podobné poli, do kterých můžeme za běhu vkládat nové prvky. Ty jsou na sebe navzájem odkazovány referencemi, jako řetěz objektů.

Založme si 2 proměnné typu Int a 2 proměnné 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)

Situace v paměti bude následující:

Referenční hodnoty ve Swift v paměti počítače - Objektově orientované programování ve Swift

Nyní zkusme přiřadit do proměnné a proměnnou b. Stejně tak přiřadíme i proměnnou v do proměnné u. Hodnotový typ se v zásobníku jen zkopíruje, u objektu se zkopíruje pouze reference (což je vlastně také hodnotový typ), ale objekt máme stále jen jeden. V kódu vykonáme tedy 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á situace vypadat následovně:

Referenční hodnoty ve Swift v paměti počítače - Objektově orientované programování ve Swift

Přesvědčme se o tom, abyste viděli, že to opravdu tak je :) Nejprve si necháme všechny čtyři proměnné vypsat před a po změně. Protože budeme výpis volat vícekrát, napíši ho poněkud úsporněji. Upravme tedy kód na následující:

// 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ýstupu programu zatím rozdíl mezi hodnotovým a referenčním typem nepoznáme:

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

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

Nicméně víme, že zatímco v a a b jsou opravdu 2 různá čísla se stejnou hodnotou, v u a v je ten samý objekt. Pojďme změnit jméno uživatele v a dle našich předpokladů by se měla změna projevit i v proměnné u. K programu připíš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
    }
}

Změnili jsme objekt v proměnné 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 se změnou v se změní i u, protože proměnné ukazují na ten samý objekt. Jestli se ptáte, jak vytvořit opravdovou kopii objektu, tak nejjednodušší je objekt znovu vytvořit pomocí konstruktoru a dát do něj stejná data. Dále můžeme použít klonování, ale o tom zas až někdy jindy. Připomeňme si situaci v paměti ještě jednou a zaměřme se na Jana Nováka.

Referenční hodnoty ve Swift v paměti počítače - Objektově orientované programování ve Swift

Co se sním stane? "Sežere" ho tzv. ARC (Automatic Reference Counting). Obrázek níže je ilustrativní, pojem Garbage Collector je obdoba ARC u ostatních jazyků.

ARC ve Swift - Objektově orientované programování ve Swift

ARC a dynamická správa paměti

Paměť můžeme v programech alokovat staticky, to znamená, že ve zdrojovém kódu předem určíme, kolik jí budeme používat. Doposud jsme to tak vlastně dělali a neměli jsme s tím problém, hezky jsme do zdrojového kódu napsali potřebné proměnné. Brzy se ale budeme setkávat s aplikacemi (a už jsme se vlastně i setkali), kdy nebudeme před spuštěním přesně vědět, kolik paměti budeme potřebovat.

V minulosti, hlavně v dobách jazyků C, Pascal a C++, se k tomuto účelu používaly tzv. pointery, neboli přímé ukazatele do paměti. Vesměs to fungovalo tak, že jsme si řekli operačnímu systému o kus paměti o určité velikosti. On ji pro nás vyhradil a dal nám její adresu. Na toto místo v paměti jsme měli pointer, přes který jsme s pamětí pracovali. Problém byl, že nikdo nehlídal, co do paměti dáváme (ukazatel směřoval na začátek vyhrazeného prostoru). Když jsme tam dali něco většího, zkrátka se to stejně uložilo a přepsala se data za naším prostorem, která patřila třeba jinému programu nebo operačnímu systému (v tom případě by naši aplikaci OS asi zabil - zastavil). Často jsme si však my v paměti přepsali nějaká další data našeho programu a program se začal chovat chaoticky. Představte si, že si uložíte uživatele do pole a v tu chvíli se vám najednou změní barva uživatelského prostředí, tedy něco, co s tím vůbec nesouvisí. Hodiny strávíte tím, že kontrolujete kód pro změnu barvy, poté zjistíte, že je chyba v založení uživatele, kdy dojde k přetečení paměti a přepsání hodnot barvy.

Když naopak nějaký objekt přestaneme používat, musíme po něm místo sami uvolnit, pokud to neuděláme, paměť zůstane blokovaná. Pokud toto děláme např. v nějaké metodě a zapomeneme paměť uvolňovat, naše aplikace začne padat, případně zasekne celý operační systém. Taková chyba se opět špatně hledá, proč program přestane po několika hodinách fungovat? Kde tu chybu v několika tisících řádků kódu vůbec hledat? Nemáme jedinou stopu, nemůžeme se ničeho chytit, musíme projet celý program řádek po řádku nebo začít prozkoumávat paměť počítače, která je v binárce. Brrr. Podobný problém nastane, když si někde paměť uvolníme a následně pointer opět použijeme (zapomeneme, že je uvolněný, to se může lehce stát), povede někam, kde je již uloženého něco jiného a tato data budou opět přepsána. Povede to k nekontrolovanému chování naší aplikace a může to dopadnout i takto:

Blue Screen Of Death – BSOD ve Windows - Objektově orientované programování ve Swift

ARC je součástí přímo aplikačního kódu a vše potřebné do naší aplikace vloží kompilátor při překladu naší aplikace. Již víme, že zkratka znamená Automatic Reference Counting, tedy automatické počítání referencí. Jakmile se počet referencí na nějaký objekt dostane na nulu, dojde k dealokaci a tedy odstranění tohoto objektu z paměti. Program se totiž dostal do fáze, kdy ví, že se k objektu nedá dostat a tudíž zbytečně zabírá místo. Později se dostaneme ještě ke klíčovému slovu weak, kterým je možné označit proměnné. Weak reference se nepočítají do "ARC počítadla" a jakmile existují pouze weak reference, dochází k dealokaci, protože tyto weak reference nebrání ARC, aby daný objekt dealokoval. Toto je nutné řešit v případě, když dvě instance na sebe drží reference a dojde k tzv. "zacyklení" a nikdy nedojde k uvolnění paměti, protože na oba objekty pořád něco odkazuje (v tomto případě vždy ten druhý objekt).

Hodnota nil

Poslední věc, o které se zmíníme (respektive si připomeneme), je tzv. hodnota nil, kterou již znáte ze základního kurzu o typu Optional. Na rozdíl od dalších jazyků, kde existuje ekvivalent v podobě null, nemůže ve Swift jen tak nějaká proměnná nabývat hodnoty nil, musí být označena jako Optional.

Když nastavíme nějakou proměnnou na nil, zrušíme pouze tu jednu referenci. Pokud na náš objekt existuje ještě nějaká reference, bude i nadále existovat. Pokud ne, bude uvolněn ARC.

K referenčním typům se ještě jednou vrátíme.

V následujícím cvičení, Řešené úlohy k 4. lekci OOP ve Swift, si procvičíme nabyté zkušenosti z předchozích lekcí.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 7x (16.21 kB)
Aplikace je včetně zdrojových kódů v jazyce Swift

 

Předchozí článek
Řešené úlohy k 3. lekci OOP ve Swift
Všechny články v sekci
Objektově orientované programování ve Swift
Přeskočit článek
(nedoporučujeme)
Řešené úlohy k 4. lekci OOP ve Swift
Článek pro vás napsal Filip Němeček
Avatar
Uživatelské hodnocení:
4 hlasů
Autor se věnuje vývoji iOS aplikací (občas macOS)
Aktivity