Pattern matching nam pomaže pisati jasniji kod te jednostavnije izraziti poslovnu logiku, a do kraja ovog blog posta ćemo vidjeti i kako nam pomaže uhvatiti bugove.
Pattern matching nam omogućuje da prepoznamo neki uzorak u podacima s kojima radimo. Na primjer, pomaže prepoznati uzorak na temelju klase podatka i vrijednost svojstva. Time nam pattern matching omogućuje da pri programiranju logiku temeljimo na obliku podataka, što ćemo i vidjeti kroz primjere u nastavku.
U C# pattern matching se prvi put pojavio u verziji C# 7, ali tada je imao mali skup mogućnosti. Pogotovo u odnosu na jezike koji duže vrijeme podržavaju pattern matching, poput u F#. Iduća verzija jezika, C# 8 je donijela nove mogućnosti pattern matchinga, ali i nova mjesta u kodu gdje ga možemo upotrijebiti.
C# 9 dodatno povećava mogućnosti pattern matchinga, a ovdje ćemo istražiti te nove mogućnosti i vidjeti kako nam pomažu u svakodnevnom radu.
Primjeri koda su najbolji način da se upoznaju nove mogućnost nekog programskog jezika. Stoga, poboljšanja pattern matching u C# 9 ćemo upoznati kroz niz primjera koda. Postoji zajednička tema koja se provlači kroz sve primjere koje ćemo ovdje koristiti: zamislimo da radimo web shop na kojemu kupci mogu dobiti različite popuste. Kod u primjerima će računati popust na temelju različitih parametara, ovisno koju mogućnost pattern matchinga želimo pokazati.
Jednostavan pattern za tip varijable (simple type patterns)
Krenimo s jednim jednostavnim unaprjeđenjem pattern matchinga: da bi smo odredili tip varijable više ne trebamo koristiti znak za odbacivanje discard ‘_’ .
Pogledajmo sljedeći primjer: 10% popusta dobije svatko tko se logira s osobnim računom u našu web aplikaciju, a pošto nam je cilj da više kompanija koristi naš web shop, svatko tko se logira s poslovnim računom dobije 20% popusta.
U C# 8 morali smo dodati znak za odbacivanje discard “_” nakon tipa podatka:
// C# 8 discountPercentage = order.Account switch { PersonalAccount _ => 0.10, CompanyAccount _ => 0.20 };
Ali, discard “_” znak u ovakvim slučajevima ne donosi neku dodatnu korisnu informaciju, pa u C# 9 pravilo je promijenjeno i sada možemo izostaviti “_” iza tipa podatka:
// C# 9 discountPercentage = order.Account switch { PersonalAccount => 0.10, CompanyAccount => 0.20 };
Relacijski patterni (relational patterns)
Relacijski patterni (relational patterns) provjeravaju je li zadana vrijednost manja, veća, manja-ili-jednaka, ili veća-ili-jednaka od zadane konstante.
U sljedećem primjeru želimo izračunati popust na temelju broja naručenih artikala. Ako kupac naruči manje od 3 artikla popust je 5%. Ako naruči od 3 do 9 artikala dati ćemo 10% popusta, a u slučaju da naruči 10 ili više artikala dobiti će 20% popusta.
Ovu poslovnu logiku mogli bi smo implementirati koristeći if-else if:
if (order.Items.Count == 0) return 0; if (order.Items.Count < 3) return 0.05; else if (order.Items.Count < 10) return 0.10; else return 0.20;
Ovaj kod odradi zadatak, ali nije najelegantniji primjer koda. Neki dijelovi koda se ponavljaju više puta, kao na primjer “order.Items.Count”. Možemo li napisati ovaj kod tako da bude konzistentniji? Na taj način imamo manje mjesta na kojima možemo slučajno napraviti bug, a uz to kasnije će developeri brže prolaziti kroz takav kod.
U C# 9 možemo koristiti sljedeća dva relacijska patterna (relational patterns): < i >= pa kod sada izgleda ovako:
discountPercentage = order.Items.Count switch { 0 => 0, <3 => 0.05, >=10 => 0.20, _ => 0.10 };
Ovakav kod je već konzistentniji, ali može se i dalje unaprijediti. Primijetite da postoci nisu poredani od najmanjeg do najvećeg: linija za 10% je na dnu te koristimo discard znak “_” da uhvatimo vrijednosti između 3 i 9, koje nisu obuhvaćene ostalim linijama.
Za daljnje poboljšanje ovog koda, upotrijebiti ćemo još jedno unaprijeđenije pattern matchinga u C# 9: logičke patterne (logical patterns).
Logički patterni (logical patterns)
Logički patterni (logical patterns) omogućuju developerima da kombiniraju druge patterne uz pomoć operatora and, or i not. Primijetite da se ovi logički operatori razlikuju od operatora &&, ||, !. Operatori and, or, not se koriste za kombiniranje patterna, dok se operatori &&, ||, ! koriste za kombiniranje Boolean vrijednosti.
Nastavimo s prethodnim primjerom računanja popusta na temelju broja artikala. Možemo koristiti and pattern da bi smo rekli “za vrijednosti veće-ili-jednake 3 i manje od 10, popust je 10%”. Možemo dodati i or pattern u ovaj primjer – umjesto < 3 možemo napisati 1 or 2.
discountPercentage = order.Items.Count switch { <=0 => 0, 1 or 2 => 0.05, >=3 and <10 => 0.10, >=10 => 0.20 };
Da bismo pokazali primjer korištenja not patterna vratiti ćemo se na prvi primjer u kojemu smo računali popust na temelju tipa računa: privatni ili poslovni račun.
U C# 8 imali smo mogućnost provjeriti je li vrijednost jednaka null:
// C# 8 discountPercentage = order.Account switch { PersonalAccount _ => 0.10, CompanyAccount _ => 0.20, null => throw new ArgumentNullException($"{nameof(order.Account)} is null.") };
Sada u C# 9 možemo koristiti not pattern i provjeriti da vrijednost nije null – u ovom primjeru možemo provjeriti da order.Account nije null, ali isto tako nije i neki od poznatih tipova računa s kojima naš kod zna raditi:
// C# 9 discountPercentage = order.Account switch { PersonalAccount => 0.10, CompanyAccount => 0.20, null => throw new ArgumentNullException($"{nameof(order.Account)} is null."), not null => throw new ArgumentException($"Unrecognized account type.") };
Moje mišljenje je da će se not pattern koristiti češće u if naredbi prilikom provjere je li vrijednost null. Koliko puta ste napisali “!= null”? U C# 9 možemo to napisati praktički na engleskom jeziku: “is not null”.
if (order is not null) { // ... }
Kad vidim “is not null” to me podsjeća na SQL gdje ovakav izraz je podržan odavno.
Redoslijed izračuna kod patterna
Kad u istom izrazu koristimo više patterna, postavlja se pitanje mogu li developeri utjecati na redoslijed izvođenja patterna?
Developeri mogu napraviti ono što je i za pretpostaviti – da bi definirali redoslijed izračuna u izrazu koji sadrži više patterna, počevši sa C# 9, samo trebaju staviti zagrade.
Dodatna prednost korištenja patterna – kompajler pronalazi bug
Što prije se otkrije bug, to je jeftinije ispraviti ga. Bug pronađen u produkciji je općenito najskuplji za ispraviti. Prema tome, bilo bi odlično pronaći bug u trenutku kada programer piše kod u kojemu se upravo pojavljuje taj bug. Još je bolje ako sam kompajler automatski pomogne i to u trenutku dok programer još piše kod.
Kada koristimo pattern matching unutar switch izraza (switch expression) kompajler može provjeriti jesu li svi slučajevi pokriveni. Da vidimo kako to funkcionira u praksi, vratimo se na primjer izračuna popusta na temelju broja artikala.
Ako promijenimo „>= 10“ u „> 10“ tada popust više neće biti definiran u slučaja da kupac naruči točno 10 artikala, ali zbog toga što koristimo pattern matching unutar switch izraza, kompajler će nas automatski upozoriti da slučaj kada je vrijednost jednaka 10 nije pokriven:
Upozorenje u Visual Studiu precizno navodi koji slučaj nije pokriven pattern matchingom u switch izrazu
Gdje u kodu možemo koristiti pattern matching?
Ovo sigurno neće biti iznenađenje: svi novi patterni se mogu koristiti na svim mjestima gdje su se i u C# 8 mogli koristiti patterni:
- switch izraz (switch expression) – kada naš C# kod treba birati između više opcija, switch expression je elegantno rješenje, a isto tako elegantno će se unutar njega uklopiti pattern matching. Dodatno kompajler će nam provjeriti jesmo li pokrili sve (rubne) slučajeve i upozoriti nas na potencijalni bug
- is pattern izraz će se često koristiti za pisanje uvjeta if naredbe, kao npr. kod provjere je li vrijednost varijable jednaka null
- case labela u switch naredbi (switch statement) – na ovim mjestima u kodu lako se vidi kako pattern matching povećava čitljivost koda
- ugniježdeni patterni (nested patterns) – mogućost kombiniranja patterna je, po mom mišljenju, nužan preduvjet da bi patterne više koristili u našim aplikacijama
Poboljšanja u C# 9
Kada pogledam kako se pattern matching razvio, mogu reći da su nova poboljšanja u C# 9 korak u pravom smjeru. Pattern matching je korisna mogućnost C# koja nam omogućuje da zapišemo poslovnu logiku na jednostavniji način, a pri tome dobijemo C# kod koji se lakše i brže čita. Uz to, kao dodatni bonus, ako koristimo pattern matching u switch izrazu kompajler može bolje analizirati kod te nas upozoriti ako jedan ili više (rubnih) slučajeva nisu pokriveni.
Godinama se podrška za pattern matching u C# poboljšava, a taj napredak će se nastaviti. Naime, pristup tima koji razvija C# je da kompleksnije mogućnosti jezika dolaze u koracima kroz više verzija jezika. Tako je s pattern matchingom, a tako će biti na primjer i s podrškom za record. Jedva čekam vidjeti koje nove mogućnosti će pattern matching dobiti u C# 10.
- Najvažnije novosti u C# 10 - 09.11.2021.
- Manje poznate mogućnosti C# 9 - 18.06.2021.
- Pattern matching u C# 9 – korak u pravom smjeru - 05.03.2021.