Lekce 2 - Filtrování a mapování polí ve Swift
V minulé lekci, Úvod do kolekcí a genericita ve Swift, jsme si vysvětlili genericitu ve Swift.
Dostáváme se k další důležité části programování. Jak již doufám víte z lekce o poli ve Swiftu, často se hodí uložit několik prvků stejného typu a poté s nimi dále pracovat. Právě k tomuto účelu kolekce existují. Swift má nabídku kolekcí velmi přímočarou a nabízí tři primární:
Array
- pole, které již známeDictionary
- slovníkSet
- množina
Každá z kolekcí se hodí k něčemu jinému, nejčastěji budete
používat Array
.
Array
Swift implementuje Array
jako moderní pole a hojně se v tomto
jazyce využívá. Jen pro zopakování nemusíme přesně určovat jeho
velikost, nijak se o ni starat, a máme k dispozici metody na pohodlné
přidávání položek, jejich odebírání a také několik dalších. Ty
základní již známe, takže je zde přeskočíme. O pokročilých metodách
polí bude právě dnešní lekce.
Založme si novou konzolovou aplikaci s názvem např. Kolekce
a
v main.swift
si připravme jednoduché pole čísel.
var cisla = [2, 5, 9, 12, 34, 17, 28, 18]
filter()
Velmi často potřebujeme pole "přefiltrovat" a získat pouze prvky, které
vyhovují nějaké podmínce. Tuto podmínku zapíšeme do speciálního bloku
nazvaného v angličtině closure. Nyní nám stačí vědět,
že se jedná o blok kódu podobný funkci, který můžeme předat nějaké
metodě jako parametr. V podstatě předáme funkci funkci, možná to zní
zmateně, ale když se nad tím zamyslíte, dává to smysl. Např. zavoláme
metodu filter()
a té řekneme, aby nám pole profiltrovala jen na
hodnoty, které odpovídají naší closure (funkci). Právě metodě
filter()
nyní opravdu předáme closure specifikující jak
profiltrovat naše pole čísel. Pokud začneme psát cisla.filter
a zbytek kódu si necháme "dopsat" od Xcode, tak dostaneme něco takového:
cisla.filter(isIncluded: (Int) throws -> Bool)
Closure se zapisuje takto pomocí šipky, již bychom tedy uměli definovat metodu, která ji bere jako argument. To my ale nechceme a proto znovu potvrdíme klávesou Enter. Xcode nám vygeneruje prázdnou closure. Výsledek vypadá takto:
cisla.filter { (Int) -> Bool in
code
}
Výraz (Int) -> Bool
označuje, že z čísla (typ
Int
) potřebujeme zjistit zda má v poli zůstat nebo ne (typ
Bool
). Int
v závorce a také "code" v bloku máme
zvýrazněné. Do závorky, kde je nyní Int
, zapíšeme název
proměnné pro jednotlivé prvky v poli, abychom s nimi mohli dále pracovat.
Jelikož v našem poli máme čísla, přejmenujeme proměnnou na
cislo
:
cisla.filter { (cislo) -> Bool in
}
Metoda filter()
pro každé číslo v našem poli provede náš
zatím prázdný closure a podle vrácené hodnoty Bool
určí,
jestli bude číslo v novém vyfiltrovaném poli. Dejme tomu, že chceme
ponechat pouze čísla větší než 10, takže do bloku napíšeme:
return cislo > 10
Výsledný kód tedy vypadá následovně:
cisla.filter { (cislo) -> Bool in return cislo > 10 }
Když výsledek vypíšeme, tak vidíme, že všechna čísla opravdu splňují tuto podmínku.
print(cisla)
Zkrácení zápisu
Možná si říkáte, že pro filtrování potřebujeme nějak zbytečně moc
kódu, když vlastně pouze porovnáváme číslo s hodnotou 10
.
Swift naštěstí nabízí o poznání zkrácený zápis, ale musíme ho napsat
celý sami.
cisla.filter { $0 > 10 }
To je mnohem lepší, že? $0
zastupuje proměnnou, která se v
našem případě jmenovala cislo
a return
není
potřeba. Je totiž jasné, že se tento výraz vrací a použije se pro
vyhodnocení návratové hodnoty. Všimněte si, že z kódu zmizely
kulaté závorky. Této syntaxi se říká tzv. trailing
closure. Pokud by měla metoda i nějaké standardní parametry, uvedli
bychom je do kulaté závorky, tu ukončili, a následně předali closure.
Mohlo by to vypadat např. takto:
instance.metoda("nějakýParametr") { $0 > 10 }
Když nyní umíme pole filtrovat, ukážeme si i podobnou a neméně
užitečnou metodu, map()
.
map()
Metoda slouží k tzv. transformaci dat na jiný datový typ nebo do jiné
struktury. V closure nám potom stačí definovat, jak chceme
existující data na nová přeměnit. Například bychom chtěli
všechna čísla vynásobit dvěma. Nejdříve si opět ukážeme, jak vypadá
vygenerovaný kód od Xcode. Z metody filter()
je vám určitě
povědomý.
cisla.map { (Int) -> T in
}
T
zde nahrazuje konkrétní datový typ a jedná se o
genericitu, kterou jsme si vysvětlili minule. Nyní nám stačí T
nahradit za datový typ, který chceme vrátit jako výsledek. Jelikož pouze
násobíme, tak výstupní typ zůstane Int
. Kompletní volání
metody map()
pro vynásobení prvků pole dvěma by vypadalo
následovně:
cisla.map { (cislo) -> Int in return cislo * 2 }
Samozřejmě jej můžeme opět zkrátit na trailing colusure:
cisla.map { $0 * 2 }
Možná vás napadá, že toto vše byste mohli udělat v nějakém cyklu i
bez znalosti closures. Bylo by to ale pracnější, bylo by potřeba založit
nové pole a jednotlivé prvky do něj vkládat. Navíc díky metodám
filter()
a map()
je při čtení kódu hned jasné, o
co se snažíme.
Násobení čísel je samozřejmě velmi základní případ.
map()
bychom spíše využili např. kdybychom měli pole objektů
Student
a chtěli získat pole jejich emailových adres.
compactMap()
Tato metoda je velmi podobná již vysvětlené map()
, ale s
důležitým rozdílem. Nevrátí nám nil
hodnoty, pokud by
nějaké měly naší transformací vzniknout. Například můžeme mít pole
typu String
, ze kterého chceme získat čísla, ale ne všechny
hodnoty lze na čísla převést.
Vytvoříme si pole řetězců, z nichž některé půjde úspěšně
parsovat na Int
, a použijeme compactMap()
:
let moznaCisla = ["21", "pět", "1", "osm", "98"] let urciteCisla = moznaCisla.compactMap { Int($0) }
Jako výsledek získáme pole: [21, 1, 98]
Kdybychom použili obyčejnou map()
metodu, tak dostaneme méně
praktický výsledek:
[Optional(21), nil, Optional(1), nil, Optional(98)]
.
reduce()
Na závěr si ukážeme ještě jednu silnou a užitečnou metodu. Jejím
smyslem je redukovat celé pole na jednu hodnotu. Takže můžeme třeba
všechna čísla sečíst, podobně jako bychom měli k dispozici metodu
sum()
z jiných jazyků.
Popíšeme si, jak reduce()
vlastně funguje a ukážeme
implementaci součtu všech prvků. Pracovat budeme s následujícím polem:
let cisla = [21, 30, 57, 1, 23, 10]
V základu nám Xcode doplní takto obsáhlou metodu, včetně placeholder textu. Ten poté nahradíme našimi názvy proměnných:
cisla.reduce(initialResult: Result, nextPartialResult: (Result, Int) throws -> Result)
Metoda reduce()
v každém kroku pracuje s předchozím
výsledkem, do kterého se postupně promítají všechny prvky pole, v našem
případě v něm bude na konci obsažený jejich součet. Prvním parametrem
určujeme počáteční hodnotu initialResult
, což bude v
případě sčítání 0
. Jako druhý parametr zadáváme closure,
který určuje jak se mají jednotlivé prvky ve výsledné hodnotě projevit,
my je k ní přičteme.
Přepíšeme si tedy placeholder text na konkrétní datové typy a
využijeme trailing closure
, kterou známe z předchozích
metod:
cisla.reduce(0) { (result, cislo) -> Int in return result + cislo }
To už začíná dávat větší smysl, že? Protože chceme sčítat, tak
začneme od 0
a v closure
vždy vezmeme předchozí
výsledek a přičteme k němu aktuální číslo. Tímto sečteme všechna
čísla v poli.
Celý zápis můžeme dramaticky zkrátit, podobně jako u předchozích metod:
cisla.reduce(0, +)
Nechali jsme 0
a celý zápis sčítání nahradili
+
operátorem, protože operátory jsou vlastně speciální typy
metod. Jak jsem psal v úvodu, reduce()
je poměrně komplexní a
nabízí hromadu možností. Cílem v tomto tutoriálu je primárně to, abyste
o její existenci věděli a znali základní použití.
V příští lekci, Slovníky (Dictionary) ve Swift, se budeme věnovat slovníkům, které Swift
implementuje jako třídu Dictionary
.