2. diel - Filtrovanie a mapovanie polí vo Swift
V minulej lekcii, Úvod do kolekcií a genericita vo Swift , sme si vysvetlili genericitu vo Swift. Dostávame sa k ďalšie dôležité časti programovania. Ako už dúfam viete z lekcie o poli vo Swiftu, často sa hodí uložiť niekoľko prvkov rovnakého typu a potom s nimi ďalej pracovať. Práve na tento účel kolekcie existujú. Swift má ponuku kolekcií veľmi priamočiarou a ponúka tri primárne:
Array
- pole, ktoré už poznámeDictionary
- slovníkSet
- množina
Každá z kolekcií sa hodí k niečomu inému, najčastejšie budete
používať Array
.
Array
Swift implementuje Array
ako moderné polia a hojne sa v tomto
jazyku využíva. Len pre zopakovanie nemusíme presne určovať jeho veľkosť,
nijako sa o ňu starať, a máme k dispozícii metódy na pohodlné pridávanie
položiek, ich odoberanie a tiež niekoľko ďalších. Tie základné už
poznáme, takže je tu preskočíme. O pokročilých metódach polí bude práve
dnešná lekcie.
Založme si novú konzolovú aplikáciu s názvom napr. Kolekce
a v main.swift
si pripravme jednoduché pole čísel.
var cisla = [2, 5, 9, 12, 34, 17, 28, 18]
Filter ()
Veľmi často potrebujeme pole "prefiltrovať" a získať iba prvky, ktoré
vyhovujú nejaké podmienke. Túto podmienku zapíšeme do špeciálneho bloku
nazvaného v angličtine closure. Teraz nám stačí vedieť,
že sa jedná o blok kódu podobný funkciu, ktorý môžeme odovzdať nejaké
metóde ako parameter. V podstate odovzdáme funkciu funkciu, možno to znie
zmätene, ale keď sa nad tým zamyslíte, dáva to zmysel. Napr. zavoláme
metódu filter()
a tej povieme, aby nám polia profiltrovala len na
hodnoty, ktoré zodpovedajú našej closure (funkciu). Práve metóde
filter()
teraz naozaj odovzdáme closure špecifikujúca ako
prefiltrovať naše pole čísel. Ak začneme písať cisla.filter
a zvyšok kódu si necháme "dopísať" od Xcode, tak dostaneme niečo
takéto:
cisla.filter(isIncluded: (Int) throws -> Bool)
Closure sa zapisuje takto pomocou šípky, už by sme teda vedeli definovať metódu, ktorá ju berie ako argument. To my ale nechceme a preto znovu potvrdíme klávesou Enter. Xcode nám vygeneruje prázdnu closure. Výsledok vyzerá takto:
cisla.filter { (Int) -> Bool in
code
}
Výraz (Int) -> Bool
označuje, že z čísla (typ
Int
) potrebujeme zistiť či má v poli zostať alebo nie (typ
Bool
). Int
v zátvorke a tiež "code" v bloku máme
zvýraznené. Do zátvorky, kde je teraz Int
, zapíšeme názov
premennej pre jednotlivé prvky v poli, aby sme s nimi mohli ďalej pracovať.
Keďže v našom poli máme čísla, premenujeme premennú na
cislo
:
cisla.filter { (cislo) -> Bool in
}
Metóda filter()
pre každé číslo v našom poli vykoná náš
zatiaľ prázdny closure a podľa vrátenej hodnoty Bool
určí,
či bude číslo v novom filtrovaného poli. Dajme tomu, že chceme ponechať
iba čísla väčšie ako 10, takže do bloku napíšeme:
return cislo > 10
Výsledný kód teda vyzerá nasledovne:
cisla.filter { (cislo) -> Bool in return cislo > 10 }
Keď výsledok vypíšeme, tak vidíme, že všetky čísla naozaj spĺňajú túto podmienku.
print(cisla)
Skrátenie zápisu
Možno si hovoríte, že pre filtrovanie potrebujeme nejako zbytočne veľa
kódu, keď vlastne iba porovnávame číslo s hodnotou 10
. Swift
našťastie ponúka o poznanie skrátený zápis, ale musíme ho napísať celý
sami.
cisla.filter { $0 > 10 }
To je oveľa lepšie, že? $0
zastupuje premennú, ktorá sa v
našom prípade vymenovala cislo
a return
nie je
potreba. Je totiž jasné, že sa tento výraz vracia a použije sa pre
vyhodnotenie návratové hodnoty. Všimnite si, že z kódu zmizli
okrúhle zátvorky. Tejto syntax sa hovorí tzv. Trailing
closure. Ak by mala metóda aj nejaké štandardné parametre, uviedli
by sme ich do okrúhle zátvorky, tú ukončili, a následne odovzdali closure.
Mohlo by to vyzerať napr. Nasledovne:
instance.metoda("nějakýParametr") { $0 > 10 }
Keď teraz vieme poľa filtrovať, ukážeme si aj podobnú a nemenej
užitočnú metódu, map()
.
Máp ()
Metóda slúži na tzv. Transformáciu dát na iný dátový typ alebo do
inej štruktúry. V closure nám potom stačí definovať, ako chceme
existujúce dáta na nové premeniť. Napríklad by sme chceli všetky
čísla vynásobiť dvomi. Najskôr si opäť ukážeme, ako vyzerá
vygenerovaný kód od Xcode. Z metódy filter()
je vám určite
povedomý.
cisla.map { (Int) -> T in
}
T
tu nahrádza konkrétny dátový typ a jedná sa o genericitu,
ktorú sme si vysvetlili minule. Teraz nám stačí T
nahradiť za
dátový typ, ktorý chceme vrátiť ako výsledok. Keďže iba násobíme, tak
výstupné typ zostane Int
. Kompletné volanie metódy
map()
pre vynásobení prvkov poľa dvoma by vyzeralo
nasledovne:
cisla.map { (cislo) -> Int in return cislo * 2 }
Samozrejme ho môžeme opäť skrátiť na trailing colusure:
cisla.map { $0 * 2 }
Možno vás napadá, že toto všetko by ste mohli urobiť v nejakom cykle aj
bez znalosti CLOSURES. Bolo by to ale pracnejšie, bolo by potrebné založiť
nové polia a jednotlivé prvky doň vkladať. Navyše vďaka metódam
filter()
a map()
je pri čítaní kódu hneď jasné,
o čo sa snažíme.
Násobenie čísel je samozrejme veľmi základné prípad.
map()
by sme skôr využili napr. keby sme mali polia objektov
Student
a chceli získať pole ich emailových adries.
CompactMap ()
Táto metóda je veľmi podobná už vysvetlené map()
, ale s
dôležitým rozdielom. Nevráti nám nil
hodnoty, ak by nejaké
mali našej transformáciou vzniknúť. Napríklad môžeme mať pole typu
String
, z ktorého chceme získať čísla, ale nie všetky hodnoty
je možné na čísla previesť.
Vytvoríme si pole reťazcov, z ktorých niektoré pôjde úspešne spracuje
na Int
, a použijeme compactMap()
:
let moznaCisla = ["21", "pět", "1", "osm", "98"] let urciteCisla = moznaCisla.compactMap { Int($0) }
Ako výsledok získame pole: [21, 1, 98]
Keby sme použili obyčajnú map()
metódu, tak dostaneme menej
praktický výsledok:
[Optional(21), nil, Optional(1), nil, Optional(98)]
.
Reduce ()
Na záver si ukážeme ešte jednu silnú a užitočnú metódu. Jej zmyslom
je redukovať celé pole na jednu hodnotu. Takže môžeme napríklad všetky
čísla sčítať, podobne ako by sme mali k dispozícii metódu
sum()
z iných jazykov.
Popíšeme si, ako reduce()
vlastne funguje a ukážeme
implementáciu súčtu všetkých prvkov. Pracovať budeme s nasledujúcim
poľom:
let cisla = [21, 30, 57, 1, 23, 10]
V základe nám Xcode doplní takto obsiahlu metódu, vrátane placeholder textu. Ten potom nahradíme našimi názvy premenných:
cisla.reduce(initialResult: Result, nextPartialResult: (Result, Int) throws -> Result)
Metóda reduce()
v každom kroku pracuje s predchádzajúcim
výsledkom, do ktorého sa postupne premietajú všetky prvky poľa, v našom
prípade v ňom bude na konci obsiahnutý ich súčet. Prvým parametrom
určujeme počiatočnú hodnotu initialResult
, čo bude v prípade
sčítanie 0
. Ako druhý parameter zadávame closure, ktorý
určuje ako sa majú jednotlivé prvky vo výslednej hodnote prejaviť, my ich k
nej pripočítame.
Prepíšeme si teda placeholder text na konkrétne dátové typy a využijeme
trailing closure
, ktorú poznáme z predchádzajúcich metód:
cisla.reduce(0) { (result, cislo) -> Int in return result + cislo }
To už začína dávať väčší zmysel, že? Pretože chceme sčítať, tak
začneme od 0
a v closure
vždy vezmeme
predchádzajúci výsledok a pripočítame k nemu aktuálne číslo. Týmto
sčítame všetky čísla v poli.
Celý zápis môžeme dramaticky skrátiť, podobne ako u predchádzajúcich metód:
cisla.reduce(0, +)
Nechali sme 0
a celý zápis sčítanie nahradili +
operátorom, pretože operátormi sú vlastne špeciálne typy metód. Ako som
písal v úvode, reduce()
je pomerne komplexný a ponúka hromadu
možností. Cieľom v tomto tutoriálu je primárne to, aby ste o jej existencii
vedeli a poznali základné použitie.
V budúcej lekcii, Slovníky (Dictionary) vo Swift , sa budeme venovať slovníkom, ktoré Swift
implementuje ako triedu Dictionary
.