A C# 7.0 újdonsága a pattern matching, ami az is
operátort emeli egy újabb szintre. Tételezzük fel, hogy a metódusunkban meg kell néznünk, hogy a kapott objektum implementálja-e az IPelda
interfészt és ha implementálja azt, akkor meg kell hívnunk a Teszt()
metódusát. Ha pedig az IMasik
interfészt implementálja, akkor a MasikTeszt()
metódust hívjuk meg. Ennek a legegyszerűbb és legbiztonságosabb módja az eddig tanultak alapján:
void Foo(object peldany)
{
var pelda = peldany as IPelda;
var masik = peldany as IMasik;
pelda?.Teszt();
masik?.MasikTeszt();
}
A fentebbi kódrészlettel azonos működést eredményez az alábbi kódrészlet:
void Foo(object peldany)
{
if (peldany is IPelda pelda)
{
pelda.Teszt();
}
if (peldany is IMasik masik)
{
masik.MasikTeszt();
}
}
Előnye a pattern matching szolgáltatásnak a null check és konvertálás mellett, hogy akár switch-case
szerkezetben is használhatjuk és nem csak értékek mentén tudunk elágazni, hanem típusok mentén is:
void Foo(object peldany)
{
switch (peldany)
{
case IPelda pelda:
pelda.Teszt();
break;
case IMasik masik:
masik.MasikTeszt();
break;
}
}
A pattern matching a C# 8 és 9-ben teljesedett ki igazán. A C# 7.0 csak deklarációs és típus patterneket támogatott. A nyelv későbbi iterációi bővítették a lehetőségeket.
Konstansok
Tételezzük fel, hogy az alkalmazásunkban meg kell állapítanunk a belépőjegy összegét a látogatók száma alapján. A leggyakrabban előforduló látogatószámhoz van egy táblázat, ami alapján ezt meg tudjuk csinálni. Mivel a bemenet a legtöbb esetben konstans, ezért a metódusunkat kézenfekvő lenne egy switch-case
szerkezettel lekódolni valahogy így:
decimal GetGroupTicketPrice(int visitors)
{
switch (visitors)
{
case 1:
return 12.0m;
case 2:
return 20.0m;
case 3:
return 27.0m;
case 4:
return 32.0m;
case 0:
return 0.0m;
default:
return 32.0m * (decimal)visitors;
}
}
A fenti kódrészlet igen szellős a switch
formázása miatt. C# 8.0 óta azonban ez helyettesíthető egy switch pattern segítségével, ami jóval tömörebb kódot eredményez:
decimal GetGroupTicketPrice(int visitors) => visitors switch
{
1 => 12.0m,
2 => 20.0m,
3 => 27.0m,
4 => 32.0m,
0 => 0.0m,
_ => 32.0m * visitors,
};
A fenti példában a _
reprezentálja hagyományos switch-case
esetén a default
ágat. Picivel pontosabban pattern matching esetén a _
egy olyan értéket jelent, ami nem releváns.
Racionális értékek
Tételezzük fel, hogy a feladatunk egy olyan metódus megalkotása, ami egy adott hőmérsékletet szöveggé konvertál. Írhatunk valami ilyesmit:
string ToString(double temperature)
{
if (!(temperature < -5.0))
{
if (!(temperature > 32.0))
{
if (double.IsNaN(temperature))
{
return "Unknown";
}
return "Acceptable";
}
return "Too Hot";
}
return "Too Cold";
}
A fenti kódrészletet átlátni picit bonyolult. Ez a bonyolultság a sok if
utasításból adódik. C# 9 óta a switch pattern intervallumokra is vonatkozhat, amivel egyrészt tömörebb, másrészről pedig átláthatóbb kódot tudunk írni:
string ToString(double temperature) => temperature switch
{
< -5.0 => "Too Cold",
> 32.0 => "Too Hot",
double.NaN => "Unknown",
_ => "Acceptable",
}
Logikai minták
Szintén a C# 9 újdonsága, hogy bevezeti az and
, or
és not
kulcsszavakat patternek esetén. Egyrészt ez olvashatóságot javít, másrészről meg lehetővé tesz igen érdekes megoldásokat. Például írjunk egy metódust, ami a hónap sorszáma alapján visszaadja az évszakot. Konstans patternnel ez viszonylag könnyen megadható, de ebben az esetben minden hónapot fel kellene sorolnunk 3x, ami redundáns. De C# 9 óta megoldhatjuk így:
string GetCalendarSeason(DateTime date) => date.Month switch
{
3 or 4 or 5 => "spring",
6 or 7 or 8 => "summer",
9 or 10 or 11 => "autumn",
12 or 1 or 2 => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};
Ezen minták esetén a kiértékelési sorrend a következő: not
, and
és végül or
. Ezen operátorok ugyanúgy zárójelezhetőek a jobb érthetőség miatt, mint bármelyik kifejezés:
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
A not
bevezetése lehetőséget ad igen beszédes null
érték ellenőrzésre is:
void NullCheck(string input)
{
if (input is not null)
{
//védett kód
}
}
Property minták
Property minták használatára C# 8 óta van lehetőség. Ez lehetővé teszi, hogy egy objektum tulajdonságai alapján döntsünk a kódunkban. Legyen például az a feladat, hogy alkotni kell egy metódust, ami egy megadott dátum alapján ellenőrzi, hogy egy ismerősünk születésnapja van-e. Ehhez hónapot és napot kell figyelnünk. C# 8 óta ezt megtehetjük így is:
bool IsBirthDay(DateTime date) => date is { Year: 2022, Month: 1, Day: 22 };
Ez a fajta ellenőrzés kifejezetten hasznos tud lenni, ha pedig logikai mintákkal kombináljuk, akkor egy zseniális szűrő eszköz:
bool IsBirthDay(DateTime date) => date is { Year: 2022, Month: 1, Day: 4 or 7 or 22 };
A tulajdonság minták rekurzívan is megadhatók. Ez azt jelenti, hogy ha a típusunk több típusból épül fel, akkor a típus bármelyik tulajdonságának tulajdonságára hivatkozhatunk.
Például legyen egy vonal típusunk, ami kezdőpontból és végpontból áll. A feladatunk pedig az, hogy írjunk egy metódust, ami igaz értéket ad vissza, ha a vonal valamelyik vége az X tengelyen van:
class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y)
{
X = x;
Y = y;
}
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
class Line
{
public Point Start { get; set; }
public Point End { get; set; }
}
bool IsAnyEndOnXAxis(Line line) => line is { Start: { Y: 0 } } or { End: { Y: 0 } }
When, var és Value tuple
C# 7 óta használhatjuk a when
és a var
kulcsszavakat egy mintán belül. Ez különösen akkor hasznos, ha egy olyan lambda metódust szeretnénk alkotni, amely a bemenet alapján valamilyen értékkel tér vissza:
Point Transform(Point point) => point switch
{
var (x, y) when x < y => new Point(-x, y),
var (x, y) when x > y => new Point(x, -y),
var (x, y) => new Point(x, y),
};
A fennti mintában a (x, y)
a típus Deconstruct
metódusát hívva egy tuple-ben tárolja a típus értékeit. A when
rész pedig egyben végzi el a feltétel-ellenőrzést és az értékadást.
Ez a fajta pattern akkor is használható, ha a típus nem rendelkezik Deconstruct
metódussal. Ebben azt esetben viszont a tuple-be alakítást nekünk kell elvégeznünk a =>
operátor után. Tehát ha a Point
típusunk nem rendelkezne a Deconstruct
metódussal, akkor az előző példa így nézne ki:
Point Transform(Point point) => (point.X, point.Y) switch
{
var (x, y) when x < y => new Point(-x, y),
var (x, y) when x > y => new Point(x, -y),
var (x, y) => new Point(x, y),
};
Ha ezt a Tuple mintát kombináljuk a korábbi mintákkal, akkor igen tömör metódusokat alkothatunk. Például viszonylag egyszerűen kiszámolhatjuk egy csoport jegyárát, ha a jegyár függ a csoport méretétől és a látogatás dátumától:
static decimal GetGroupTicketPriceDiscount(int groupSize, DateTime visitDate)
=> (groupSize, visitDate.DayOfWeek) switch
{
(<= 0, _) => throw new ArgumentException("Group size must be positive."),
(_, DayOfWeek.Saturday or DayOfWeek.Sunday) => 0.0m,
(>= 5 and < 10, DayOfWeek.Monday) => 20.0m,
(>= 10, DayOfWeek.Monday) => 30.0m,
(>= 5 and < 10, _) => 12.0m,
(>= 10, _) => 15.0m,
_ => 0.0m,
};
A when
kulcsszó kivételkezelők definiálásakor is alkalmazható. Itt segítségével a kivétel tulajdonságai alapján és nem csak típusa alapján tudjuk specifikálni a kivételkezelő blokk működésbe lépésének feltételét.
EEz akkor jön jól, ha a kód, amivel dolgozunk ugyanazt a típusú kivételt generálja több esetben, de ezekből csak specifikus eseteket szeretnénk lekezelni, mondjuk az üzenet szövege alapján. A .NET keretrendszerben ilyen típus a HttpClient
1, ami HttpRequestException
kivételeket dob. A HTTP pedig különböző státuszkódokkal dolgozik. A szerver, vagy REST API, amit meg akarunk hívni pedig 500-as hibát dob, ha a szerver túlterhelt. Ha más hibakódot kapunk vissza, akkor azt szimplán nem kezeljük le. Ezt a következő módon tudjuk megtenni:
var client = new HttpClient();
var streamTask = client.GetStringAsync("https://localHost:10000");
try
{
await streamTask;
Console.WriteLine(streamTask);
}
catch (HttpRequestException e) when (e.Message.Contains("500"))
{
Console.WriteLine("A szerver túlterhelt");
}
Megjegyzés: Az await
kulcsszó a 10. fejezetben kerül elő részletesen. A példaprogram azért alkalmazza, mivel ez a minta kifejezetten a when
miatt került be.
Lista minták
C# 11 óta egy tömb vagy lista elemeire tudunk logikai mintát illeszteni. Tételezzük fel, hogy adott egy tömbünk:
int[] numbers = { 1, 2, 3 };
Ezen az alábbi minta igaz eredményt fog adni, mivel a tömb elemei 1, 2 és 3 egymást követően.
numbers is [1, 2, 3];
A mintákban a tömb hossza is ellenőrzésre kerül, vagyis a három elemű tömbünk nem felel meg a numbers is [1, 2, 3, 4]
mintának. A minták logikát is tartalmazhatnak:
numbers is [0 or 1, <= 2, >= 3]
A mintákban használhatjuk a _
jelet, ha egy elem mindegy, illetve a ..
operátort, ha több elemet hagynánk ki az illesztésből. Például az is [3, _, .., 0, 1]
minta minden olyan tömbre igaz lesz, amely legalább 4 elemű és 3-mal kezdődik, majd a 0, 1 számokkal végződik.
A lista minta minden olyan típusra alkalmazható egyébként, amely rendelkezik egy olvasható indexer tulajdonsággal és egy Length
vagy Count
tulajdonsággal, ami egy egész számot ad vissza:
class Teszt
{
public int Length { get; }
public Teszt(int length)
{
Length = length;
}
public int this[int i] => i;
}
var t = new Teszt(3);
bool eredmeny = t is [0, 1, 2]; // true;