NOVINKA! E-learningové kurzy umělé inteligence. Nyní AI za nejlepší ceny. Zjisti více:
NOVINKA – Víkendový online kurz Software tester, který tě posune dál. Zjisti, jak na to!

Lekce 10 - Vlastnosti ve Swift

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

V dnešním Swift tutoriálu se podíváme na slíbené deklarování pokročilejších vlastností.

Gettery a settery

Velmi často se nám stává, že chceme mít kontrolu nad změnami nějaké vlastnosti objektu zvenčí. Budeme chtít vlastnost nastavit jako read-only nebo reagovat na její změny. Založme si nový projekt (název Vlastnosti) a vytvořme následující třídu Student, která bude reprezentovat studenta v nějakém informačním systému.

class Student: CustomStringConvertible {
    var jmeno: String
    var muz: Bool
    var vek: Int
    var plnolety: Bool

    init(jmeno: String, pohlavi: Bool, vek: Int) {
        self.jmeno = jmeno
        self.muz = pohlavi
        self.vek = vek
        plnolety = true
        if vek < 18 {
            plnolety = false
        }
    }

    var description: String {
        var jsemPlnolety = "jsem"
        if (!plnolety) {
            jsemPlnolety = "nejsem"
        }
        var pohlavi = "muž"
        if (!muz) {
            pohlavi = "žena"
        }
        return "Jsem \(jmeno), \(pohlavi). Je mi \(vek) let a \(jsemPlnolety) plnoletý."
    }

}

Třída je velmi jednoduchá, student se nějak jmenuje, je nějakého pohlaví a má určitý věk. Podle tohoto věku se nastavuje vlastnost plnolety pro pohodlnější vyhodnocování plnoletosti na různých místech systému. K uložení pohlaví používáme hodnotu Bool, zda je student muž. Konstruktor dle věku určí, zda je student plnoletý. Vlastnost description je navržena pro potřeby tutoriálu tak, aby nám vypsala všechny informace. V reálu by vrátila pravděpodobně jen jméno studenta. Pomocí konstruktoru si nějakého studenta vytvořme:

let s = Student(jmeno: "Pavel Hora", pohlavi: true, vek: 20)
print(s)
class Student: CustomStringConvertible {
    var jmeno: String
    var muz: Bool
    var vek: Int
    var plnolety: Bool

    init(jmeno: String, pohlavi: Bool, vek: Int) {
        self.jmeno = jmeno
        self.muz = pohlavi
        self.vek = vek
        plnolety = true
        if vek < 18 {
            plnolety = false
        }
    }

    var description: String {
        var jsemPlnolety = "jsem"
        if (!plnolety) {
            jsemPlnolety = "nejsem"
        }
        var pohlavi = "muž"
        if (!muz) {
            pohlavi = "žena"
        }
        return "Jsem \(jmeno), \(pohlavi). Je mi \(vek) let a \(jsemPlnolety) plnoletý."
    }

}

Výstup:

Jsem Pavel Hora, muž. Je mi 20 let a jsem plnoletý.

Vše vypadá hezky, ale vlastnosti jsou přístupné jak ke čtení, tak k zápisu. Objekt tedy můžeme rozbít například takto (hovoříme o nekonzistentním vnitřním stavu):

let s = Student(jmeno: "Pavel Hora", pohlavi: true, vek: 20)
s.vek = 15
s.muz = false
print(s)
class Student: CustomStringConvertible {
    var jmeno: String
    var muz: Bool
    var vek: Int
    var plnolety: Bool

    init(jmeno: String, pohlavi: Bool, vek: Int) {
        self.jmeno = jmeno
        self.muz = pohlavi
        self.vek = vek
        plnolety = true
        if vek < 18 {
            plnolety = false
        }
    }

    var description: String {
        var jsemPlnolety = "jsem"
        if (!plnolety) {
            jsemPlnolety = "nejsem"
        }
        var pohlavi = "muž"
        if (!muz) {
            pohlavi = "žena"
        }
        return "Jsem \(jmeno), \(pohlavi). Je mi \(vek) let a \(jsemPlnolety) plnoletý."
    }

}

Výstup:

Jsem Pavel Hora, žena. Je mi 15 let a jsem plnoletý.

Určitě musíme ošetřit, aby se plnoletost obnovila při změně věku. Když se zamyslíme nad ostatními vlastnostmi, není nejmenší důvod, abychom je taktéž umožňovali modifikovat. Student si za normálních okolností asi jen stěží změní pohlaví nebo jméno. Bylo by však zároveň vhodné je vystavit ke čtení, nemůžeme je tedy pouze pouze nastavit jako private. V dřívějších lekcích Swift kurzu jsme k tomuto účelu používali metody, které sloužily ke čtení privátních vlastností. Jejich název jsme volili jako vratVek() a podobně. Ke čtení vybraných vlastností vytvoříme také metody a vlastnosti označíme jako privátní, aby se nedaly modifikovat zvenčí. Třída by nově vypadala např. takto (vynechal jsem konstruktor a description):

class Student {
    var jmeno: String
    var muz: Bool
    var vek: Int
    var plnolety: Bool

    // ...

    func vratJmeno() -> String {
        return jmeno
    }

    func vratPlnoletost() -> Bool {
        return plnolety
    }

    func vratVek() -> Int {
        return vek
    }

    func jeMuz() -> Bool {
        return muz
    }

    func nastavVek(hodnota: Int) {
        vek = hodnota
        // přehodnocení plnoletosti
        plnolety = true
        if vek < 18 {
            plnolety = false
        }
    }

}

Metody, co hodnoty jen vracejí, jsou velmi jednoduché. Nastavení věku má již nějakou vnitřní logiku, při jeho změně musíme totiž přehodnotit vlastnost plnolety. Zajistili jsme, že se do proměnných nedá zapisovat jinak, než my chceme. Máme tedy pod kontrolou všechny změny vlastností a dokážeme na ně reagovat. Nemůže se stát, že by nám někdo vnitřní stav nekontrolovaně měnil a rozbil.

Metodám k navrácení hodnoty se říká gettery a metodám pro zápis settery. Pro editaci ostatních vlastností bychom udělali např. jednu metodu editujStudenta(), která by byla podobná konstruktoru. Jméno, věk a podobně by se tedy měnily pomocí této metody, tam bychom mohli např. kontrolovat, zda hodnoty dávají smysl, opět bychom odchytili všechny pokusy o změnu na jediném místě.

Ptaní se na vlastnosti pomocí metod je ovšem pracné a může být i matoucí. Swift proto obsahuje pro vlastnosti další syntaxi.

private(set)

Pokud chceme pouze zamezit tomu, aby bylo vlastnosti možné nastavovat zvenčí, pomůže nám modifikátor přístupu private(set). Ten nyní umístíme před vlastnosti naší třídy, zachováme tím perfektní zapouzdření a gettery (metody) pro tyto vlastnosti již nebudeme potřebovat, proto je odstraníme.

private(set) var jmeno: String
private(set) var muz: Bool
private(set) var vek: Int

Uvnitř třídy se vlastnosti chovají standardně, ale zvenčí je nemůžeme měnit.

Computed properties

Elegantně jsme zamezili nechtěným modifikacím vlastností naší třídy zvenčí. Jinou technikou můžeme docílit také toho, že se vlastnost chová jako metoda, ale když se na ní ptáme, nepíšeme za jejím názvem závorku (). To se hodí pro vlastnosti, které vracejí hodnotu na základě jiných vlastností. Takovýmto vlastnostem ve Swift říkáme Computed properties. V naší třídě je využijeme pro plnoletost, kde místo uložené Bool hodnoty vrátíme přímo výraz vek < 18. Tím bude hodnota vlastnosti vždy aktuální. Podobně funguje i vlastnost description, kterou jsme doteď používali spíše intuitivně. Vzpomeňte si, že vracela vždy aktuální textovou podobnu instance na základě dalších vlastností. Upravme tedy vlastnost plnolety:

var plnolety: Bool {
    return vek >= 18
}

Takto tedy bude vypadat třída po všech úpravách výše:

class Student: CustomStringConvertible {

    private(set) var jmeno: String
    private(set) var muz: Bool
    private(set) var vek: Int
    var plnolety: Bool {
        return vek >= 18
    }

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

    var description: String {
        var jsemPlnolety = "jsem"
        if (!plnolety) {
            jsemPlnolety = "nejsem"
        }
        var pohlavi = "muž"
        if (!muz) {
            pohlavi = "žena"
        }
        return "Jsem \(jmeno), \(pohlavi). Je mi \(vek) let a \(jsemPlnolety) plnoletý."
    }
}

Tímto jsme napsali nejjednodušší getter, který zvenčí vypadá jako obyčejná vlastnost, ale lze pouze číst. Lepší název proměnné by byl jePlnoletý, aby odpovídal konvencím pro Bool hodnoty, ale my řešíme vlastnosti obecně :-)

Samozřejmě, pokud byste v getteru měli náročnější výpočet a přistupovali k vlastnosti často, tak se vyplatí zamyslet nad optimalizací a výsledek počítat při nastavení vlastností, z kterých vychází. V našem případě je to vlastnost vek.

Vypadalo by to např. takto:

private var _vek: Int
private(set) var plnolety: Bool

var vek : Int {
    get {
        return _vek
    }
    set {
        plnolety = newValue >= 18
        _vek = newValue
    }
}

V tomto případě si musíme vytvořit ještě proměnnou _vek, která bude držet samotný věk a při jeho nastavení skrze vek také vypočítáme, jestli je student plnoletý. Tato forma se ale prakticky nepoužívá a tyto settery a gettery slouží vyloženě pro computed properties. newValue, jak jste asi poznali, zde reprezentuje právě nově přiřazovanou hodnotu.

Sledování změn vlastností

Často je dobré umět reagovat, když dojde ke změně nějaké vlastnosti. Např. chceme přepočítat další vlastnosti, spustit metodu, pokud byla dosažena určitá hranice nebo např. aktualizovat uživatelské rozhraní.

Swift nám umožňuje na přiřazení nové hodnoty snadno reagovat a možnost se jmenuje property observers. Máme k dispozici klíčová slova willSet a didSet. Ta se píší do složených závorek jako v případě getterů a setterů a jejich názvy prozrazují, co dělají. willSet bude zavoláno těsně před nastavením nové hodnoty do vlastnosti a didSet po jejím nastavení. Prakticky asi nikdy nepoužijete oba zároveň a didSet se používá častěji.

Můžeme si to opět ukázat na příkladu s plnoletostí.

private(set) var plnolety: Bool

private(set) var vek : Int {
    didSet {
        plnolety = vek >= 18
    }
}

Jakmile dojde k nastavení věku, přepočítá se i plnoletost. V tomto případě tradiční settery a gettery skutečně nebyly nejlepší volba. Zde záleží na vás, jestli se vám více líbí didSet řešení nebo vlastnost plnolety s jednoduchým getterem pouze pro čtení. U didSet a willSet mějte pouze na paměti, že se bohužel nezavolají, pokud je hodnota nastavena z konstruktoru, tudíž v našem případě musíme v konstruktoru studenta plnoletost poprvé nastavit sami.

Nastavování věku a plnoletosti si můžeme ještě otestovat, přidáme si jednoduchou metodu:

func nastavVek(_ vek: Int) {
    self.vek = vek
}

A upravíme main.swift:

let s = Student(jmeno: "Pavel Hora", pohlavi: true, vek: 20)
print(s)
s.nastavVek(17)
print(s)
class Student: CustomStringConvertible {
    private(set) var jmeno: String
    private(set) var muz: Bool

    private(set) var plnolety: Bool

    private(set) var vek : Int {
        didSet {
            plnolety = vek >= 18
        }
    }

    init(jmeno: String, pohlavi: Bool, vek: Int) {
        self.jmeno = jmeno
        self.muz = pohlavi
        self.vek = vek
        plnolety = true
        if vek < 18 {
            plnolety = false
        }
    }

    var description: String {
        var jsemPlnolety = "jsem"
        if (!plnolety) {
            jsemPlnolety = "nejsem"
        }
        var pohlavi = "muž"
        if (!muz) {
            pohlavi = "žena"
        }
        return "Jsem \(jmeno), \(pohlavi). Je mi \(vek) let a \(jsemPlnolety) plnoletý."
    }

    func vratJmeno() -> String {
        return jmeno
    }

    func vratPlnoletost() -> Bool {
        return plnolety
    }

    func vratVek() -> Int {
        return vek
    }

    func jeMuz() -> Bool {
        return muz
    }

    func nastavVek(hodnota: Int) {
        vek = hodnota
        // přehodnocení plnoletosti
        plnolety = true
        if vek < 18 {
            plnolety = false
        }
    }

    func nastavVek(_ vek: Int) {
        self.vek = vek
    }
}

Výstup:

Jsem Pavel Hora, muž. Je mi 20 let a jsem plnoletý.
Jsem Pavel Hora, muž. Je mi 17 let a nejsem plnoletý.

Vlastnosti budeme odteď používat stále, umožňují nám totiž objekty dokonale zapouzdřit. Pro Swift jsou prakticky všechny proměnné tříd vlastnosti, poté už záleží, jak jsou konkrétně implementované. Nezapomeňte na užitečný a jednoduchý modifikátor private(set) :-)

V příští lekci, Protokoly (rozhraní) ve Swift, se podíváme jak se ve Swift pracuje s protokoly.


 

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 6x (21.12 kB)
Aplikace je včetně zdrojových kódů v jazyce Swift

 

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