Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
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 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áme
  • Dictionary - slovník
  • Set - 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.


 

Předchozí článek
Úvod do kolekcí a genericita ve Swift
Všechny články v sekci
Kolekce ve Swift
Přeskočit článek
(nedoporučujeme)
Slovníky (Dictionary) ve Swift
Článek pro vás napsal Filip Němeček
Avatar
Uživatelské hodnocení:
5 hlasů
Autor se věnuje vývoji iOS aplikací (občas macOS)
Aktivity