Kihagyás

C# alapok II.

Előkészítés

Első lépésként hozzunk létre egy .NET C# konzolalkalmazást: a projektsablon szűrőben válasszuk a C# nyelv - Windows platform - Console projekttípust. A szűrt listában válasszuk a Console App sablont (ne a .NET Framework-ös legyen). A neve legyen HelloCSharp2. A solutiont ne tegyük külön mappába (Place solution and project in the same directory legyen bekapcsolva). A megcélzott framework verzió legyen .NET 8.

Legfelső szintű utasítások, implicit globális névtér-hivatkozások

Csodálkozzunk rá, hogy a generált projekt mindössze egyetlen érdemi sort tartalmaz.

Console.WriteLine("Hello, World!");

C# 10 óta a program belépési pontját adó forrásfájlt jelentősen lerövidíthetjük:

  • a fájl tetején lévő using-okat elhagyhatjuk, ha azok implicit hivatkozva vannak. Az implicit hivatkozott using-ok projekttípustól függenek és a dokumentációból olvashatjuk ki
  • a Main függvényt tartalmazó osztály deklarációját (namespace blokk, class blokk) elhagyhatjuk, ezt a fordító generálja nekünk
  • a Main függvény deklarációját szintén generálja a fordító. A metódus neve nem definiált, nem (biztos, hogy) Main. A metódus szignatúrája attól függ, milyen utasításokat adunk meg a forrásfájlban. Például, ha nincs return, akkor void visszatérési értékű. A paramétere viszont mindig string[] args.
  • a függvény blokkba nem foglalt kód a generált belépési pont függvény belsejébe kerül. Függvényt is írhatunk, az a belépési pontot tartalmazó generált osztály tagfüggvénye lesz.
  • típusokat, osztályokat is definiálhatunk, de csak a legfelső szintű kódot követően

Warning

Fontos észrevétel a fentiekből: ezen képesség nem változtatja meg a C# semmilyen alapvető jellemzőjét, például ugyanúgy minden függvénynek osztályon belül kell lennie. A fordítás során a legfelső szintű utasítások kódja úgy egészül ki, ami már minden szabálynak megfelel.

Láthatóság

A legfelső szintű kód olyan, amit a program más részéről nem tudunk hívni, hiszen nem is ismerjük a burkoló osztály nevét. Emiatt nincs értelme legfelső szintű kódban láthatósági beállításnak (private, protected stb.) vagy propertynek.

Akadályozzuk meg a program azonnali lefutását egy blokkoló hívással.

Console.WriteLine("Hello, World!");
Console.ReadLine();

Próbáljuk ki a generált projektet mindenféle egyéb változtatás nélkül, fordítás (menu:projekten jobbklikk[Build]) után. Nézzünk bele a kimeneti könyvtárba (menu:projekten jobbklikk[Open Folder in File Explorer], majd menu:bin[Debug > net8.0]): látható, hogy az alkalmazásunkból a fordítás során egy cross-platform bináris (\<projektnév>.dll) és .NET Core v3 óta egy platform specifikus futtatható állomány (Windows esetén \<projektnév>.exe) is generálódik. Kipróbálhatjuk, hogy az exe a szokott módon indítható (pl. duplaklikkel), míg a dll a dotnet paranccsal.

dotnet <projektnév.dll>

Parancssor aktuális mappája

A dotnet parancshoz a dll könyvtárában kell lennünk. Ehhez a legegyszerűbb, ha a Windows fájlkezelőben a megfelelő könyvtárban állva az elérési útvonal mezőt átírjuk a cmd szövegre, majd kbd:[ENTER]-t nyomunk.

Adjunk a létrejövő projekthez egy Dog osztályt Dog.cs néven, ez lesz az adatmodellünk:

public class Dog
{
    public string Name { get; set; }
    public Guid Id { get; } = Guid.NewGuid();
    public DateTime DateOfBirth { get; set; }
    private int AgeInDays => DateTime.Now.Subtract(DateOfBirth).Days;
    public int Age => AgeInDays / 365;
    public int AgeInDogYears => AgeInDays * 7 / 365;
    public override string ToString() =>
            $"{Name} ({Age} | {AgeInDogYears}) [ID: {Id}]";
}

Az adatmodell az előző órán létrehozotthoz nagyon hasonlít, ennek viszont nincsen explicit konstruktora és a Name és DateOfBirth tulajdonságok publikusan is állíthatók.

Hozzunk létre egy Dog példányt objektum inicializációs szintaxissal, majd írjuk ki ezt a példányt a kezdeti köszöntő szöveg helyett:

Dog banan = new Dog
{
    Name = "Banán",
    DateOfBirth = new DateTime(2014, 06, 10)
};
Console.WriteLine(banan);

Ezzel kész a kiinduló projektünk.

Implicit típusdeklaráció

A var kulcsszó jelentősége: ha a fordító ki tudja találni a kontextusból az értékadás jobb oldalán álló érték típusát, nem szükséges a típus nevét explicit megadnunk, az implicit következik a kódból. Ebben az esetben a típus egyértelműen Dog. Ha csak deklarálni szeretnénk egy változót (nem adunk értékül a változónak semmit), akkor nem használhatjuk a var kulcsszót, ugyanis nem következik a kódból a változó típusa. Ekkor explicit meg kell adnunk a típust.

Dog banan = new Dog
{
   Name = "Banán",
   DateOfBirth = new DateTime(2014, 06, 10)
};
var watson = new Dog { Name = "Watson" };

var unnamed = new Dog { DateOfBirth = new DateTime(2017, 02, 10) };
var unknown = new Dog { };
//watson = 3; // 
//var error;  // 

Console.WriteLine(banan);
Console.ReadLine();
  • Fordítási hiba: a watson deklarációjakor eldőlt, hogy ő Dog típus, utólag nem lehet megváltoztatni és például számértéket értékül adni. Ez nem JavaScript.
  • Fordítási hiba: implicit típust csak úgy lehet deklarálni, ha egyúttal inicializáljuk is. Az inicializációs kifejezés alapján dől el (implicit) a példány típusa.

Próbáljuk ki a nem forduló sorokat, nézzük meg a fordító hibaüzeneteit!

Erőss típusoság

A var nem a gyenge típusosság jele a C#-ban, nem úgy, mint pl. JavaScript-ben. Az inicializációs sor után a típus egyértelműen eldől, utána már csak ennek a típusnak megfelelő műveletek végezhetők, például egy értékadással nem változtathatjuk meg a típust.

A var-t tipikusan akkor alkalmazzuk, ha:

  • hosszú típusneveket nem akarunk kiírni
  • feleslegesnek tartjuk az inicializáció mindkét oldalán kiírni ugyanazt a típust
  • anonim típusokat használunk (később)

Init-only setter

Az objektum inicializáció működéséhez szükséges a megfelelő láthatóságú setter. Viszont egy ilyen settert nem csak objektum inicializációkor lehet használni, hanem bármikor átállíthatjuk egy példány adatát (mutáció).

Az alábbi példa egy ilyen utólagos módosításra / mutációra.

var watson = new Dog { Name = "Watson" };
watson.Name = "Sherlock";

Ez így hiba nélkül lefordul.

Kizárólag az inicializációra korlátozhatjuk a setter meghívását az init-only setterrel (init kulcsszó).

public class Dog
{
    public string Name { get; init; }
    //...
}

Ezután az inicializációs sor továbbra is lefordul, de a névátírásos már nem. Ez utóbbi sort kommentezzük ki.

Init-only setter konstruktorból

Init-only settert az osztály konstruktorából is meg lehet hívni - hiszen az is inicializáció.

Használata

Init-only settert több okból kifolyólag is használhatunk, például a típus példányainak immutábilis kezelését akarjuk kikényszeríteni, vagy csak inicializációra akarjuk korlátozni a propertyk beállítását, de nem akarunk ehhez konstruktort írni.

Jelen formájában az init-only setter nem tudja helyettesíteni a kötelező konstruktor paramétert, mert nem kötelező kitölteni ezt a propertyt. Erre a megoldás a C# 11-ben bevezetett required kulcsszó a property előtt.

public class Dog
{
    public required string Name { get; init;  }
    //...
}

Ezzel kötelezővé válik a Name kitöltése, ha a Dog példányt inicializáljuk.

Indexer operátor, nameof operátor, index inicializáló

A collection initializer analógiájára jött létre az index initializer nyelvi elem, ami a korábbihoz hasonlóan sorban hív meg egy operátort, hogy már inicializált objektumot kapjunk vissza. A különbség egyrészt a szintaxis, másrészt az ilyenkor meghívott metódus, ami az index operátor.

operátor felüldefiniálás

Saját típusainkban lehetőségünk van definiálni és felüldefiniálni operátorokat, mint pl. +, -, indexelés, implicit cast, explicit cast, stb.

Tegyük fel, hogy egy kutyához bármilyen, üzleti logikában nem felhasznált információ kerülhet, amire általános struktúrát szeretnénk. Vegyünk fel a Dog osztályba egy string-object szótárat, amiben bármilyen további információt tárolhatunk! Ezen felül állítsuk be a Dog indexerét, hogy az a Metadata indexelését végezze:

public class Dog
{
    //...
    public Dictionary<string, object>  Metadata { get; } = new();
    public object this[string key]
    {
        get { return Metadata[key]; }
        set { Metadata[key] = value; }
    }
}

Konstruktor típus nélkül

A new operátor utáni konstruktorhívás sok esetben elhagyható, ha a bal oldal alapján amúgy is tudható a típus.

névtér hivatkozások

Az újabb projektsablonok sokkal kevesebb névtérdeklarációt (using) generálnak alapból. Ha kell, vegyük fel a szükségeseket a fel nem oldott néven állva a gyorsművelet (villanykörte) eszközzel (Ctrl+.)

Az objektum inicializáló és az index inicializáló vegyíthető, így az alábbi módon tudunk felvenni további tulajdonságokat a kutyákhoz a legfelső szintű kódba:

var pimpedli = new Dog
{
    Name = "Pimpedli",
    DateOfBirth = new DateTime(2006, 06, 10),
    ["Chip azonosító"] = "123125AJ"
};

Mivel indexelni általában kollekciókat szokás (tömb, lista, szótár), ezért ezekben az esetekben igen jó eszköz lehet az index inicializáló. Vegyünk fel egy új kutyaszótárt a kutyák kitenyésztése után:

var dogs = new Dictionary<string, Dog>
{
    ["banan"] = banan,
    ["watson"] = watson,
    ["unnamed"] = unnamed,
    ["unknown"] = unknown,
    ["pimpedli"] = pimpedli
};

foreach (var dog in dogs)
    Console.WriteLine($"{dog.Key} - {dog.Value}");

Próbáljuk ki - minden név-kutya párt ki kell írnia a szótárból.

Elsőre jó ötletnek tűnhet kiváltani a szövegliterálokat a Name property használatával.

var dogs = new Dictionary<string, Dog>
{
    [banan.Name] = banan,
    [watson.Name] = watson,
    [unnamed.Name] = unnamed,
    [unknown.Name] = unknown,
    [pimpedli.Name] = pimpedli
};
//ArgumentNullException!

Ez azonban kivételt okoz, amikor a kutya neve nincs kitöltve, azaz null értékű. Esetünkben elég lenne az adott változó neve szövegként. Erre jó a nameof operátor.

var dogs = new Dictionary<string, Dog>
{
    [nameof(banan)] = banan,
    [nameof(watson)] = watson,
    [nameof(unnamed)] = unnamed,
    [nameof(unknown)] = unknown,
    [nameof(pimpedli)] = pimpedli
};

Ez a változat már nem fog kivételt okozni.

A nameof operátor sokfajta nyelvi elemet támogat, vissza tudja adni egy változó, egy típus, egy property vagy egy függvény nevét is.

A szótár feltöltését megírhatjuk kollekció inicializációval is. Ehhez kihasználjuk, hogy a szótár típus rendelkezik egy Add metódussal, amelyik egyszerűen egy kulcsot és egy hozzátartozó értéket vár:

var dogs = new Dictionary<string, Dog>
{
    { nameof(banan), banan },
    { nameof(watson), watson },
    { nameof(unnamed), unnamed },
    { nameof(unknown), unknown },
    { nameof(pimpedli), pimpedli }
};

Using static

Ha egy osztály statikus tagjait vagy egy statikus osztályt szeretnénk használni, lehetőségünk van a using static kulcsszavakkal az osztályt bevonni a névfeloldási logikába. Ha a Console osztályt referáljuk ilyen módon, lehetőségünk van a rajta levő metódusok meghívására az aktuális kontextusunkban anélkül, hogy az osztály nevét kiírnánk:

using System;
using static System.Console;
//..
foreach (var dog in dogs)
    /*Console.*/WriteLine($"{dog.Key} - {dog.Value}");
/*Console.*/WriteLine(banan);
/*Console.*/ReadLine();

névfeloldás

Az általános névfeloldási szabály továbbra is él: ha egyértelműen feloldható a hivatkozás, akkor nem szükséges kitenni a megkülönböztető előtagot (itt: osztály), különben igen.

Nullozható típusok

Természetesen a referenciatípusok mind olyan típusok, melyek vehetnek fel null értéket, viszont esetenként jó volna, ha a null értéket egyébként felvenni nem képes típusok is lehetének ilyen értékűek, ezzel pl. jelezvén, hogy egy érték be van-e állítva vagy sem. Pl. egy szám esetén a 0 egy konkrét, helyes érték lehet a domain modellünkben, a null viszont azt jelenthetné, hogy nem vett fel értéket.

Vizsgáljuk meg, hogy a konzolra történő kiíráskor miért lesz az aktuális év Watson kutya életkora! Valamelyik Console.WriteLine sorhoz vegyünk fel egy töréspontot (F9), majd debuggolás közben a Locals ablakban (debuggolás közben menu:Debug[Windows > Locals]) figyeljük meg az egyes példányok adatait. Watsont kinyitva láthatjuk, hogy a turpisság abból fakad, hogy a DateOfBirth adat típusa, a DateTime nem referenciatípus, és alapértelmezés szerinti értéket veszi fel, ami 0001. 01. 01. 00:00:00 - hiszen nem állítottunk be mást.

Ismeretlen születési dátumú, korú egyedek helyes tárolásához az Age tulajdonság típusát változtassuk int?-re! Az int? szintaktikai édesítőszere a Nullable<int>-nek, egy olyan struktúrának, ami egy int értéket tárol, és tárolja, hogy az be van-e állítva vagy sem. A Nullable<int> szignatúráit megmutathatjuk, hogyha a kurzort a típusra helyezve F12-t nyomunk.

Módosítsuk a Dog Age és DateOfBirth tulajdonságait is, hogy tudjuk, be vannak-e állítva az értékeik:

public class Dog
{
    //...

    public DateTime? DateOfBirth { get; set; }
    private int? AgeInDays => (-DateOfBirth?.Subtract(DateTime.Now))?.Days;
    public int? Age => AgeInDays / 365;
    public int? AgeInDogYears => AgeInDays * 7 / 365;

    //...
}

Aritmentikai operátorok

Örvendezzünk, hogy az alap aritmetikai operátorok pont úgy működnek, ahogy szeretnénk (null bemenetre null eredmény), nem kellett semmilyen trükk.

Az AgeInDays akkor ad vissza null értéket, ha a DateOfBirth maga is null volt. Tehát ha nincs megadva születési dátumunk, nem tudunk életkort sem számítani. Ennek kifejezésére használhatjuk a ?. (Elvis, magyarban Kozsó - null conditional operator) operátort: a kiértékelendő érték jobb oldalát adja vissza, ha a bal oldal nem null, különben null-t. A kifejezést meg kellett változtatnunk, hogy a DateOfBirth-ből vonjuk ki a jelenlegi dátumot és ezt negáljuk, ugyanis a null vizsgálandó érték a bináris operátor bal oldalán kell, hogy elhelyezkedjen.

Elvis operátor

Az Elvis operátor nevének eredetére több magyarázatot is lehet találni, a források annyiban nagyrészt megegyeznek, hogy a kérdőjel tekeredő része az énekes jellegzetes bodorodó hajviseletére emlékeztet, a pontok pedig a szemeket jelölik, így végülis a ?. egy Elvis emotikonként fogható fel. Ezen logika mentén adódik a magyar megfelelő, a Kozsó operátor, hiszen a szem körül tekergőző legikonikusabb hajtincs a magyar zenei kultúrában Kozsó nevéhez köthető.

Ha így futtatjuk az alkalmazást, az AgeInDays és a származtatott tulajdonságok értéke null (vagy kiírva üres) lesz, ha a születési dátum nincs megadva.

Rekord típus

A rekord típusok speciális típusok, melyek:

  • egyenlőségvizsgálat során érték típusokra jellemző logikát követnek, azaz két példány akkor egyenlő, ha adataik egyenlőek
  • könnyen immutábilissá tehetők, könnyen kezelhetők immutábilis típusként

A Dog típus ezzel szemben jelenleg:

  • nem immutábilis, hiszen a születési dátum bármikor módosítható (sima setter)
  • egyenlőségvizsgálat során a normál referencia szerinti összehasonlítást követ

Az automatikusan generálódó egyedi azonosítót iktassuk ki a Dog osztályból, hogy az adat alapú összehasonlítást könnyebben tesztelhessük.

public Guid Id { get; } = Guid.Empty;

Vegyünk fel egy logikailag megegyező példányt.

var watson = new Dog { Name = "Watson" };
var watson2 = new Dog { Name = watson.Name };

Ismét álljunk meg debug során valamelyik WriteLine soron. A Locals ablakban nézzük meg, hogy a két példány minden adata megegyezik. A Watch ablakban (debuggolás közben menu:Debug[Windows > Watch > Watch 1]) értékeljük ki a watson == watson2 kifejezést. Láthatjuk, hogy ez az egyenlőségvizsgálat hamist ad, ami technikailag helyes, mert két különböző memóriaterületről van szó, a referenciák nem ugyanoda mutatnak a memóriában. Sok esetben azonban nem ezt szeretnénk, hanem például a dupla rögzítés elkerülésére az adatok alapján történő összehasonlítást, ami érték típusoknál van. Referencia típusoknál klasszikusan ezt a GetHashCode, Equals függvények felüldefiniálásával értük el (vagy az IComparable<T>, IComparer<T> interfészre épülő logikákkal). Egy újabb lehetőség a rekord típus használata.

Pozíció alapú megadás

Vegyünk fel a Dog típus adatainak megfelelő rekord típust, mindössze egy kifejezésként. A Dog típus alá:

public record class DogRec(
    Guid Id,
    string Name,
    DateTime? DateOfBirth=null,
    Dictionary<string, object> Metadata=null
);

Note

A record class jelölőből a class elhagyható.

Ez az ún. pozíció alapú megadási forma, ami a leginkább rövidített megadási formája a rekord típusnak. Ebből a rövid formából, mindenfajta extra kód írása nélkül a fordító számos dolgot generál:

  • a zárójelen belüli felsorolásból konstruktort és dekonstruktort
  • a zárójelen belüli felsorolás alapján propertyket get és init tagfüggvényekkel
  • alapértelmezett logikát az érték szerinti összehasonlításhoz
  • klónozó és másoló konstruktor logikákat
  • alapértelmezett formázott kiírást, szöveges reprezentációt (ToString implementációt)

Így egy könnyen kezelhető, immutábilis, az összehasonlításokban érték típusként viselkedő adatosztályunk lesz.

Warning

Az Id-nek nem tudjuk beállítani ebben a formában az alapértelmezett Guid.Empty értéket vagy a Metadata-nak az új példányt, mert az egyenlőségjeles kifejezésekből alapértelmezett konstruktorparaméter-értékek lesznek, amik csak statikus, fordítási időben kiértékelhető kifejezések lehetnek.

Vegyünk fel a többi Watson példány mellé két újabbat, de itt már az új rekord típusunkat használjuk.

var watson3 = new DogRec(Guid.Empty, "Watson");
var watson4 = new DogRec(Guid.Empty, "Watson");

A fentebbi Watch ablakos módszerrel ellenőrizzük a watson3 == watson4 kifejezés értékét. Ez már igaz érték lesz az adatmező alapú összehasonlítási logika miatt.

Próbáljuk ki ugyanezt a kiértékelést az alábbi változattal:

var watson3 = new DogRec(Guid.Empty, "Watson");
var watson4 = new DogRec(Guid.Empty, "Watson", DateTime.Now.AddYears(-1));

Ez hamis értéket ad, az egyenlőségnek minden mezőre teljesülnie kell, nem csak a mindkettőben kitöltöttekre.

A DogRec típus alapvetően immutábilis, a példányainak alapadatai inicializálás után nem módosíthatók. Próbáljuk felülírni a nevet.

var watson3 = new DogRec(Guid.Empty, "Watson");
var watson4 = new DogRec(Guid.Empty, "Watson", DateTime.Now.AddYears(-1));
watson4.Name = watson3.Name + "_2"; //<= nem fordul

Nem fog lefordulni, mert minden property init-only típusú. A sor jobboldala egyébként lefordulna, tehát a lekérdezés (getter hívás) működne.

Ha immutábilis típusokkal dolgozunk, akkor mutáció helyett új példányt hozunk létre megváltoztatott adatokkal. Alapvetően ezt az OO nyelvekben másoló konstruktorral oldjuk meg. A rekord típusnál ennél is továbbmenve másoló kifejezést használhatunk.

var watson4 = new DogRec(Guid.Empty, "Watson", DateTime.Now.AddYears(-1));
var watson5 = watson4 with { Name = "Sherlock" };
WriteLine(watson4);
WriteLine(watson5);

Futtatáskor a konzolban gyönyörködjünk a rekord típusok alapértelmezetten is olvasható szöveges kiírásában.

A másoló kifejezésben a with operátor előtt megadjuk, melyik példányt klónoznánk, majd az inicializáció részeként milyen értékeket állítanánk át, ehhez az objektum inicializációs szintaxist használhatjuk. Fontos eszünkbe vésni, hogy a másolás eredményeként új példány jön létre, új memóriaterület foglalódik le. Gondoljunk erre akkor, amikor egy ciklusban használjuk ezt a módszert sok egymást követő módosításra.

Mire is jó a rekord típus

Mire jó a rekord típus, az immutabilitás? Az immutábilis típussokkal való hatékony és eredményes munka másfajta, az imperatív nyelvekhez szokott fejlesztők számára szokatlan módszereket kíván. Vannak területek, ahol ez a befektetés megtérül, ilyen például a többszálú környezet. A legtöbb szálkezeléssel kapcsolatos probléma ugyanis a szálak által közösen használt adatstruktúrák mutációjára vezethető vissza (ún. race condition, versenyhelyzet). Nincs mutáció - nincs probléma. (No mutation - no cry)

Kitérő: a szótár visszavág

A rekord típus által biztosított kellemes tulajdonságok csak akkor érvényesek, ha nem keverjük hagyományos referencia típusokkal.

A szokásos módszerrel ellenőrizzük le, hogy a watson5 == watson6 kifejezés igaz-e. Igen, hiszen minden kitöltött adatuk egyezik.

var watson4 = new DogRec(Guid.Empty, "Watson", DateTime.Now.AddYears(-1));
var watson5 = watson4 with { Name = "Sherlock" };
var watson6 = watson4 with { Name = "Sherlock" };
WriteLine(watson4);
WriteLine(watson5);
WriteLine(watson6);

Vigyünk be egy ártatlan inicializációt a Metadata propertyre.

var watson4 = new DogRec(Guid.Empty, "Watson", DateTime.Now.AddYears(-1));
var watson5 = watson4 with { Name = "Sherlock", Metadata = new Dictionary<string, object>() };
var watson6 = watson4 with { Name = "Sherlock", Metadata= new Dictionary<string, object>() };
WriteLine(watson4);
WriteLine(watson5);
WriteLine(watson6);

Ezzel eléggé illogikus módon hamisra változik a watson5 == watson6 kifejezés. Az oka az, hogy a Metadata szótár egy klasszikus referencia típus, az összehasonlításnál a klasszikus memóriacím-összehasonlítás történik, viszont az a két új szótár példány esetében eltérő lesz. A formázott szöveges kiírásba is belerondít a szótár, mert ott is a szótár típus alapértelmezett szöveges reprezentációja jut érvényre, ami a típus neve.

Klónozzunk tovább, aztán próbáljunk mutációt végrehajtani a Metadata szótáron.

var watson6 = watson4 with { Name = "Sherlock", Metadata = new Dictionary<string, object>() };
var watson7 = watson6 with { Name = "Watson" };
watson7.Metadata.Add("Chip azonosító", "12345QQ");
WriteLine(watson4);

Ez lefordul, pedig ez mutáció. A Locals ablakban figyeljük meg a watson6 és watson7 szótárait: mindkettőbe bekerült a chip azonosító. Ez az ún. shallow copy jelenség, amikor nem a szótár memóriaterülete klónozódik, csak a rá mutató referencia, ami azt eredményezi, hogy a két példánynak közös szótára lesz.

Összességében az adatstruktúránkban megjelenő klasszikus referencia típus elrontja:

  • az immutabilitást
  • az érték szerinti összehasonlítást
  • a formázott szöveges megjelenést
  • a klónozást

Immutabilitás

Immutábilis környezetben törekedjünk arra, hogy a teljes adatstruktúránk támogassa az immutábilis kezelést.

Normál megadás

Ha nincs szükségünk a kikényszerített immutabilitásra, akkor használhatjuk a rekord normál megadását. Fogjuk a Dog osztályt, másoljuk le a kódját, adjunk neki más nevet és class helyett record jelölőt.

A Dog osztály fölé:

public record DogRecExt
{
    public string Name { get; init; }
    public Guid Id { get; } = Guid.Empty;
    public DateTime? DateOfBirth { get; set; }
    public Dictionary<string, object> Metadata { get; } = new();

    private int? AgeInDays => (-DateOfBirth?.Subtract(DateTime.Now))?.Days;
    public int? Age => AgeInDays / 365;
    public int? AgeInDogYears => AgeInDays * 7 / 365;

    public object this[string key]
    {
        get { return Metadata[key]; }
        set { Metadata[key] = value; }
    }
}

ToString

A ToString implementációját elhagytuk az előző szakaszban említettek miatt.

A Program.cs-be:

var watson8 = new DogRecExt { Name = "Watson" };
watson8.DateOfBirth = DateTime.Now.AddYears(-15);
var watson9 = watson8 with { };
WriteLine(watson8);
WriteLine(watson9);

Ellenőrizzük le a rekord tulajdonságokat:

  • A konzol kimeneten a formázást, továbbá a mutáció működését, azaz a watson8 születési dátuma a beállított lesz.
  • Ez nem csoda, hiszen a property deklarációban engedtük a mutációt.
  • A konzol kimeneten megfigyelt példányadatokon a klónozó kifejezés működését. Semmi különös, ugyanúgy működik, mint a tömör formánál.
  • A Watch ablakban watson8 == watson9 egyenlőséget. Ez igaz, mert minden adattagjuk egyezik.

record struct

A rekordoknak további válfajai vannak, ugyanis struktúra is lehet rekord, ilyenkor a record struct kulcsszó párt használjuk a típus deklarációjánál. Sőt, a readonly record struct egy immutábilis record struct. Ezen válfajok nyilván különbözőképpen viselkednek, mely viselkedéseket itt most nem részletezzük, de a dokumentációban megtalálhatók.


2024-02-13 Szerzők