Swift jest bardzo rozbudowanym językiem programowania, który oddaje w ręce programistów wiele narzędzi do pracy z kodem. Jednym z nich jest Pattern matching, czyli możliwość porównywania wartości na podstawie określonego wzorca. Jest to chyba jedna z najczęściej pomijanych funkcji Swift’a, która może jednak znacznie poprawić czytelność naszego kodu.
Na podstawie przykładów pokażę Wam jak korzystać z pattern matching oraz kiedy może się ono przydać 🤓.
Zanim przejdziemy do omawiania szczegółów, zobaczmy jak wygląda najprostszy przykład wykorzystujący pattern matching do spółki z guard:
1 2 3 4 5 6 7 8 9 10 11 12 |
func checkTupleValue(tuple: (foo: String, bar: String)) -> String { guard case ("Foo", "Bar") = tuple else { return "It's not soo Foo Bar" } return "We have Foo Bar!" } checkTupleValue(tuple: ("Foo", "Bar")) // wyświetli "We have Foo Bar!" checkTupleValue(tuple: ("Hello", "World")) // wyświetli "It's not soo Foo Bar" |
Funkcja checkTupleValue() jako jedyny argument przyjmuje obiekt, który określany jest jako Tuple. Składa się on z dwóch parametrów typu String. Wewnątrz funkcji znajduje się to co najistotniejsze – porównywanie wzorca z wykorzystaniem operatora case. W połączeniu z operatorem guard sprawdzamy, czy poszczególne wartości argumentu tuple pokrywają się z podanym wzorcem – („Foo”, „Bar”). Jeżeli wynik jest pozytywny to wyświetlamy odpowiednią informację w konsoli. W przeciwnym wypadku wyświetlamy informację o niepowodzeniu.
Innym przykładem zastosowania pattern matching, z którym się już zapewne nieświadomie zetknęliście (jeżeli pracowaliście już ze Swift’em) jest instrukcja switch, o której będzie za chwilę 👍.
Tuple jest prostą strukturą, która grupuje inne obiekty tworząc w ten sposób nowy typ danych. Bardzo przydatne, gdy chcemy na przykład zwrócić z funkcji więcej niż jeden parametr.
Wykorzystanie wzorców
Wzorców możemy używać w zestawieniu z wyrażeniami for, if, while, guard oraz switch. Zasada działania jest prosta – każda wartość posiada określoną strukturę (wzorzec), na podstawie której można dokonać porównania. Poniżej możecie zobaczyć jak pattern matching działa z wykorzystaniem poszczególnych wyrażeń. Przykład z guard był zaprezentowany na samym początku, więc tutaj go pominiemy. Podany kod najlepiej umieszczać w pliku Playground.
Switch
Switch pozwala nam na porównanie kilku wzorców w jednym miejscu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
func kindOfProgrammer(skills: (android: Bool, ios: Bool, nodejs: Bool)) -> String { switch skills { case (false, true, false): return "iOS programmer" case (true, false, false): return "Android programmer" case (true, false, true): return "Full-stack programmer" case (true, true, true): return "Very cool programmer" default: return "Maybe other kind of programmer?" } } kindOfProgrammer(skills: (true, false, true)) // wyświetli "Full-stack programmer" kindOfProgrammer(skills: (false, true, false)) // wyświetli "iOS programmer" kindOfProgrammer(skills: (false, false, false)) // wyświetli "Maybe other kind of programmer?" |
Dzięki instrukcji switch możemy sprawdzić kilka wzorców w ramach jednego bloku. Z uwagi na swoją konstrukcję, daje nam on również gwarancję pokrycia wszystkich możliwych przypadków lub użycie domyślnej wartości tam gdzie jest to konieczne.
Instrukcja For
Konstrukcja z for jest bardzo prosta. Podajemy konkretną wartość, którą chcemy znaleźć:
1 2 3 4 5 |
let programmers = ["iOS", "Android", "Nodejs", "Angular", "React", "iOS", "iOS"] for case "iOS" in programmers { print("We have iOS programmer") // informacja zostanie wyświetlona trzy razy } |
Instrukcja If
Wykorzystanie instrukcji if jest podobne do switch, jednak bez wszystkich zalet tej drugiej. Ucierpi też znacznie czytelność naszego kodu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func kindOfProgrammer(skills: (android: Bool, ios: Bool, nodejs: Bool)) -> String { if case (false, true, false) = skills { return "iOS programmer" } else if case (true, false, false) = skills { return "Android programmer" } return "Maybe other kind of programmer?" } kindOfProgrammer(skills: (false, true, false)) // wyświetli "iOS programmer" kindOfProgrammer(skills: (false, false, false)) // wyświetli "Maybe other kind of programmer?" |
Dostępne rodzaje wzorców
Powyżej mogliście zobaczyć proste przykłady zastosowania pattern matching, a teraz dowiecie się dokładnie z jakich wzorców będziecie mogli korzystać. Zaczniemy do dobrze wszystkim znanej enumeracji 😎.
Enumeration case pattern
W tym przypadku sprawdzana będzie wartość enumeracji:
1 2 3 4 5 6 7 8 9 |
enum Programmers { case ios, android, nodejs, angular } let programmer = Programmers.angular if case .angular = programmer { print("We have Angular programmer here!") } |
Nic skomplikowanego. Jeżeli wartość enumeracji została ustawiona na .angular, to w konsoli wyświetli odpowiednią informację.
Value-binding pattern
Polega na użyciu słowa kluczowego var lub let podczas sprawdzania wzorca. Wartość zmiennej / stałej może zostać następnie użyta wewnątrz bloku kodu:
1 2 3 4 5 |
let programmers = ("iOS", "Android") if case (let ios, "Android") = programmers { print("We have a match for \(ios) programmer") } |
Możemy też powiązać kilka zmiennych / stałych jednocześnie umieszczając odpowiednie słowo kluczowe przed nawiasem:
1 2 3 4 5 |
let programmers = ("iOS", "Android", "Angular") if case let (ios, android, "") = programmers { print("We have a match for \(ios) and \(android) programmer") } |
Wildcard pattern
Wildcard pattern pozwala określić na jakich wartościach nie będzie nam zależało. W poniższym przykładzie dajemy do zrozumienia, że nie interesują nas informacje związane z programistą Angular:
1 2 3 4 5 |
let programmers = ("iOS", "Android", "Angular") if case ("iOS", "Android", _) = programmers { print("We don't need info about Angular programmer") } |
Rzutowanie za pomocą „Is”
Za pomocą operatora is możemy sprawdzić, czy obiekt dziedziczy po danym typie:
1 2 3 4 5 6 7 8 9 10 11 12 |
let mixValues: [Any] = ["Android", true, 20] for element in mixValues { switch element { case is Bool: print("We have a Boolean!") case is UIViewController: print("We have a UIViewController!") default: print("We have something, but I don't know what :(") } } |
Wzorzec ten możemy jednak trochę rozwinąć.
Warunkowe rzutowanie za pomocą „As”
Połączenie is oraz wzorca value-binding pattern (czyli standardowej kombinacji if-let). Dzięki jego zastosowaniu możemy uzyskać dostęp do wartości przechowywanej w rzutowanym obiekcie, o ile oczywiście takie rzutowanie będzie możliwe:
1 2 3 4 5 6 7 8 9 10 |
let mixValues: [Any] = ["Android", true, 20] for element in mixValues { switch element { case let boolean as Bool: print("Value for boolean is: \(boolean)") default: print("We have something, but I don't know what :(") } } |
Optional pattern
Pozwala nam na odseparowanie wartości nil:
1 2 3 4 5 |
let programmers: [String?] = ["iOS", nil, "Android", nil] for case let programmer? in programmers { print(programmer) // wyświetli dwie wartości - "iOS" oraz "Android" } |
Słowo na drogę.
Jak sami widzicie pattern matching nie jest specjalnie skomplikowaną koncepcją i może w prosty sposób przyczynić się do poprawy czytelności naszego kodu. Skrywa on jeszcze kilka fajnych funkcji, ale te przedstawię już w osobny wpisie, bo ten zrobił się trochę przydługi 😜. Do usłyszenia 🧐.