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:
{SWIFT} let s = Student(jmeno: "Pavel Hora", pohlavi: true, vek: 20) print(s) {/SWIFT}
{SWIFT} 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ý." } } {/SWIFT}
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):
{SWIFT} let s = Student(jmeno: "Pavel Hora", pohlavi: true, vek: 20) s.vek = 15 s.muz = false print(s) {/SWIFT}
{SWIFT} 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ý." } } {/SWIFT}
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
:
{SWIFT} let s = Student(jmeno: "Pavel Hora", pohlavi: true, vek: 20) print(s) s.nastavVek(17) print(s) {/SWIFT}
{SWIFT} 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 } } {/SWIFT}
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