Vizsgáljuk meg a következő példát az összetett típusokkal foglalkozó korábbi Összetett típusok és példányaik című leckéből.
Két összetett típusunk létezik a #C-ban:
- Struktúra -
struct
- Osztály -
class
- (Igazából van egy harmadik is, amivel most nem foglalkozunk: Rekod -
record
)
struct Dog
{
public string name; // Neve
public int age; // Kora
public float speed; // Sebessége
public bool doesBite; // Harap-e?
}
class Goblin
{
public int health; // Életei
public bool isEnemy; // Ellenfél-e?
public int damage; // Sebzése
public float range; // Lőtáv
}
Mint látható, mindkettő összetett típus tartalmazhat tetszőleges számú és típusú belső változót, azaz mezőt. Most hozzunk létre egyet-egyet mindkettőből: struktúra: Dog és osztály: Goblin.
// Hozzunk létre egy-egy példát a fenti típusokból!
Dog dog1 = new Dog { name = "Blöki", age = 5, speed = 12f, doesBite = false };
Goblin goblin1 = new Goblin { health = 100, isEnemy = true, damage = 5, range = 2};
// Kiírok bizonyos értékeket: // (Unity alatt ezt Debug.Log()-gal lehet)
Colsole.WriteLine(dog1.name ); // Blöki
Colsole.WriteLine(goblin1.health); // 100
// Ezután változtassunk néhány értéken!
dog1.name = "Fifi";
goblin1.health = 70;
// Kirom az új értékeket:
Colsole.WriteLine(dog1.name); // Fifi
Colsole.WriteLine(oblin1.health); // 70
// Semmi meglepő eddig...
// Most tegyük őket át egy új változóba!
Dog dog2 = dog1;
Goblin goblin2 = goblin1;
// Most változtassunk néhány értéken az új változókban lévő objektumokon!
dog2.name = "Hektor";
goblin2.health = 50;
// Újra írjuk ki az eredeti változókban lévő objektumok értékeit!
Colsole.WriteLine(dog1.name); // Fifi NEM jelenik meg változás
Colsole.WriteLine(goblin1.health); // 50 MEGJELENIK a változás
// Hmmm.... Ez érdekes. A goblinunk élete változott, de akutyánk kora nem.
Ennek az az oka, hogy a Dog
egy struktúra ami egy érték típus míg a Goblin
egy osztály, ami ezzel szemben referencia típus. De mit is jelent ez.
Sokféleképp lehet definiálni a típusokat C#-ban azonban a legfontosabb kategória az, hogy a típus referencia vagy értékként viselkedik. Ezáltal két nagy kategóriája létezik a típusoknak:
- Referencia típusok - Az összes osztály (
class
) ilyen - Érték típusok - Az összes osztály (
struct
) ilyen
Ezen típus kategóriák máshogy viselkednek. Most tekintsük át a kategóriákat és viselkedésüket egyenként!
Az érték típusok és átadásuk
Ha az objektumokra úgy gondolunk, mint valós fizikai tárgyakra, akkor hasznos lehet az érték típusú változókra pedig úgy tekinteni, mint fizikai tárolókra, afféle “dobozokra”, amik tartalmazhatnak egy-egy objektumot. Ilyen dobozba betehetünk egy tárgyat és idővel cserélhetjük is a tartalmát, de egy objektum csak egyetlen dobozban lehet és soha sem többen. Pont úgy ahogy ez a valóságban is van: Egy tárgy nem lehet két helyen. Az érték típusú változó és a benne tartott objektum tehát egy az egyben megfeleltethető egymásnak.
A már eddig megismert primitív típusok közül érték az int
, float
, a bool
. Majd minden még nem tanult beépített típusra is igaz lesz ez. (string
az egyik kivétel. Látsd később)
Emellett minden struct
összetett típus is értékként viselkedik. Például az olyan Unity-ben gyakran hasznát struktúrák, mint a Vector2
, Vector3
, Quaternion
, Color
mind érték típusúak.
Egy már definiált érték típusú változóban garantáltan mindig lesz valamilyen valós érték. A dobozba ha már egyszer betettünk valamit, az utána nem lehet sosem üres. Ha egy ilyen változóban tárolt objektumot megpróbáljuk átadni egy másik változónak, akkor az eredeti értéknek egy másolata készül: egy új objektum. Így garantálja a C# hogy egyik dobozunk sem lesz üres.
Ez a viselkedés igen kényelmes használatot eredményez. Nem szükséges, például egy int
vagy Vector3
változó esetén, hogy mindig ellenőrizgessem, hogy tartalmaz-e értéket. Garantált, hogy a “dobozban” lesz is valami. El sem tudjuk rontani.
Ez a másolás miatt tehát az érték típusoknál paraméterátadás esetén úgy tekinthető, hogy csak az objektum értéke adódik át, nem az objektum maga. Innen adódik a neve: Érték típus.
Vegyük a következő példákat.
int someInteger = 42; // Az int érték típus
Dog someDog = new Dog{name = "Toto"}; // A dog struktúra, tehát érték típus
Fent létrehoztam egy …
- …
int
típusú objektumot, a 42-t és azt értékül adtam asomeInteger
változónak. - …
Dog
típusú objektumot, Toto-t és azt értékül adtam asomeDog
változónak.
int someOtherInteger = someInteger; // Az int érték típus
someOtherInteger = 999; // Ha az egyik változó értékén módosítunk,
// annak semmi hatása nincs a másikra.
Console.WriteLine(someInteger); // 42
// --------------------------------------------------------------------------------
Dog someOtherDog = someDog; // A Dog struktúra, tehát érték típus
someOtherDog.name = "Morzsa"; // Ha az egyik változó értékén módosítunk,
// annak semmi hatása nincs a másikra.
Console.WriteLine(someDog.name); // Toto
Az érték típusok általában kisebb, egyszerűbb dolgokat reprezentálnak, pl.: számok vektorok.
A referencia típusok és átadásuk
Ezzel szemben a referencia típusú változó és a benne tartott objektum kapcsolata lazább, a változó független a benne tárolt objektumtól. Ha valós világ béli analógiát keresünk, akkor érdemes inkább úgy tekinteni rá, mint egy “mutatóra” ami, megmutatja, hol van az objektum. Ebben az esetben, akár a valóságban, több mutató és mutathat egyazon objektumra.
Referencia típus a string
, valamint minden tömb és lista függetlenül attól, hogy mi a bennük tárolt típus. Emellett referencia típus minden osztály (class
) is. Ebbe beletartozik a legtöbb Unity specifikus típus GameObject
, Mesh
, Sprite
, Texture
, Material
valamint minden komponens, amely kategóriába beleértendő minden általunk írt MonoBehaviour
szkript is. Vegyük a következő példát.
// A Goblin osztály, tehát referencia típus
Goblin someGoblin = new Goblin{health = 100};
Fent létrehoztam egy Goblin
típusú objektumot, amit értékül adtam a someGoblin
változónak.
Ha ezt az objektumot megpróbáljuk átadni egy másik változónak, akkor nem készül másolat az objektumból, hanem az új változó is az eredetire és az egyetlen Goblin
objektumra fog hivatkozni.
Goblin someOtherGoblin = someGoblin; // A Goblin osztály, tehát érték típus
someOtherDog.health -= 25; // Ha az egyik változó értékén módosítunk,
// azt a másik változón is érzékeljük
Console.WriteLine(someGoblin.health); // 75
// Ugyanis mindkét változó ugyanarra
// az egy objektumra hivakozik.
A referencia típusoknál paraméterátadás esetén az objektum referenciája adódik csak át, nem készül új példány. (Innen a neve)
A referencia típusok általában komplikáltabb dolgokat reprezentálnak, ahol nem akarjuk, hogy gyakran megtörténjen az érték típusokra jellemző objektum másolás, egy egyszerű paraméter átadás esetén. Ez feleslegesen sok processzormunkát és memóriát igényelne.
Változók és objektumok a gyakorlatban
Gyakran összemosódik ez a két fogalom: változók és objektum, de érdemes mentálisan jól elkülöníteni őket. Most nézzük át a fenti példát a megtanultak tükrében úgy, hogy közben vezetjük (jobb oldalt), hogy milyen változóink és objektumaink léteznek a memóriában.
// Hozzunk létre egy-egy példát a fenti típusokból!
Dog dog1; // ÚJ VÁLTOZÓT deklaráltunk
dog1 = new Dog { name = "Blöki" }; //ÚJ OBJEKTUM
Goblin goblin1; // ÚJ VÁLTOZÓT deklaráltunk
goblin1 = new Goblin { health = 100 }; //ÚJ OBJEKTUM
// Most tegyük őket át egy új változóba!
// ÉRTÉKTÍPUS ESETÉN:
Dog dog2 = dog1; // Új változót deklaráltunk,
// és másolódik az eredeti objektum.
// A másolat kerül az új változóba
// REFERENCIATPUS ESETÉN:
Goblin goblin2 = goblin1; // Új változót deklaráltunk,
// de új objektum nem jött létre
// Az eredetiret mutat az új változó is
// Most módosítsunk az új változókban lévő objektumokon!
// Írjuk ki az eredeti változókban lévő objektumok értékeit!
dog2.name = "Hektor"; // ÉRTÉKTÍPUS ESETÉN:
Colsole.WriteLine(dog1.name); // Blöki (NINCS VÁLTOZÁS)
// Mert két Dog objektumunk létezik.
// Egyiket nevét változtattuk meg
// és a másik nevét írtuk ki.
goblin2.health = 50;
Colsole.WriteLine(goblin1.health); // 50 (VAN VÁLTOZÁS)
// Mert egy Goblin objektumunk van
// és arra mutat mindkét változó
Most vegyünk át még néhány különbséget a két típusfajta közt majd végül próbáljuk meg összefoglalni, miért léteznek ezek a kategóriák és, hogy mikor melyik használata lehet célravezetőbb. ⛔
Na de miért létezik ez a két kategória?
Ennek megválaszolásához még meg kell érteni egy két dolgot…
Metódusok paraméterátadása referencia- és értéktípusok esetében
Vegyük például az alábbi két eljárást. Az egyiknek a Dog
, egy érték típusú struct
az egyik bemenő paramétere, míg a másiknak egy Goblin
, ami pedig egy referencia típusú class
.
void MakeDogAge(Dog dog, int years) // Kutyát idősítő metódus
{
dog.age += years; // Idősebbé tesszük a kutyát valamennnyi évvel
}
void DamageGoblin(Goblin goblin, int damage) // Goblint sebző metódus
{
goblin.health -= damage; // Sebezzük a goblin-t valamennnyivel
}
És most próbáljuk meg használni ezen metódusokat:
// Most létrehozok egy-egy példát a fenti típusokból.
Dog dog1 = new Dog { name = "Blöki", age = 5, speed = 12f, doesBite = false };
Goblin goblin1 = new Goblin { health = 100, isEnemy = true, damage = 5, range = 2};
// Majd egy metódus segítségével megpróbálok változtatni az értékeiken
MakeDogAge(dog, 2);
DamageGoblin(goblin, 20);
// Újra írjuk ki az értékeket:
Colsole.WriteLine(dog.age); // 5 NEM SIKERÜLT: A kutya még mindig 5 éves
Colsole.WriteLine(goblin.health); // 80 SIKERÜLT: A goblinnak már csak 80 élete van
Ennek oka, hogy a MakeDogAge
és a DamageGoblin
metódusok fejlécében lévő dog
és goblin
változókba ugyanúgy történt az a paraméterátadás, mint a fentebbi példákban, amikor egyszerű változók tartalmát adtuk át egymásnak. Az érték típusú dog
-nak csak egy másolatát módosítjuk tehát a metóduson belül viszont a referencia típusú goblin
esetében az eredeti példányt.
Szóval a MakeDogAge
metódusunk habár szintaktikailag helyes, olyan értelemben hibás, hogy semmire sem használható. A következőképp lehetne viszont átírni, hogy legyen is értelme.
Dog MakeDogAge(Dog dog, int years) // Kutyát idősítő metódus
{
dog.age += years; // Idősebbé tesszük a kutyát valamennnyi évvel
return dog; // Visszatérünk vele
}
Ezután így kéne használni:
dog = MakeDogAge(dog, 2);
Gondoljuk végig, hogy például a System.Math
és a UnityEngine.Mathf
matematikai osztályok ilyen jellegű függvényeket tartalmaznak.
float a = -45;
// Nem elég leírni ezt:
Math.Abs(a); // HIBÁS
// Helyette:
a = Math.Abs(a);
Ennek oka, hogy ezen matematikai osztályok kizárólag számokkal végeznek különböző műveleteket, amik mind érték típusok: int
, float
… Vissza is kell térniük egy eredménnyel, ahhoz, hogy bármi hatása legyen a használatuknak.
Objektumok életciklusa Garbage Collection
Láttuk hogy egy objektumra mutathat egy változó, de több is. Mi van akkor viszont, ha egy objektumra már egy változó már sem mutat? Változók mindig rendelkeznek életciklussal. Ha egy metóduson belül deklaráltuk például egy változót, akkor a függvény lefutásával megszűnik létezni a változó. Ekkor a tartalma sem lesz többé hozzáférhető. Mivel objektumokat mindig csak változókon keresztül értünk el, ezért biztosak lehetünk benne, hogy az objektumot, amire már nem mutat egy változó sem, nem is fogjuk tudni többet használni. Lehet hogy létezik még a memóriában, a szoftverünk azonban garantáltan nem fogja többet igénybe venni. Fel lehet szabadítani a megfelelő memóriaterületet és fel is kell ha nem akarunk nagyon hamar kifogyni belőle.
Ezt a felszabadítást a C# automatikusan végzi. Ha az adott objektum érték típusú, akkor ez a felszabadítási művelet egyszerű. Emlékezzünk: Egy érték típusú változó egyértelműen megfeleltethető a benne tárolt objektummal, tehát egy érték objektum csak egy érték változóban lehet. Ezért az érték típusú objektumok életciklusa az őket tartalmazó változó életciklusához kötött. Mikor egy érték változó megszűnik létezni, akkor a benne tárolt objektum memóriaterületét is egyből felszabadítja a C#.
Nem ilyen egyszerű a kérdés referencia objektumok esetén, amikről nem tudhatja kapásból a fordító, hogy létezik-e máshol eltárolva. Egy változó megszüntetésével nem törölhetjük automatikusan a benne tárolt objektumot, hisz nem tudhatjuk, hogy más változó használja-e. Ezek az objektumok tehát nem kerülnek egyből felszabadításra. Helyette a C# folyamatosan számon tart egy adatbázist arról, hogy hány változónk és hány objektumunk létezik. Ha úgy ítéli meg hogy sokkal nagyobb a mérete az objektumoknak, mint ami a változók összességében elfér, akkor tudja, hogy sok a felszabadítatlan objektum. Ekkor egy úgynevezett szemétgyűjtést azaz Garbage Collection-t hajt végre.
A szemétgyűjtés folyamán a Garbage Collector átnézi az összes referencia objektumot és megvizsgálja, hogy van-e legalább egy változó, amin keresztül elérhető az objektum. Ha nem talál ilyen változót, akkor és csak akkor szabadítja fel a megfelelő memóriát. Ez egy relatíve időigényes folyamat. Sok feltételtől függően hosszú ezred- vagy századmásodperceket is igénybe vehet. (Igen, ez soknak számít). A Garbage Collection automatikusan történik meg a háttérben, a fejlesztőnek nem kell kiadni semmilyen parancsot hozzá. Ez bizonyos tekintetben nagyon kényelmes használatot eredményez. Fejlesztőként C# alatt sokszor gondolnunk sem kell a memóriafelszabadításra.
Azonban a Garbage Collector használata nem csak előny, de hátrány is lehet. A probléma, hogy sok elvégzendő memóriafelszabadítási munka adódik össze egy rövid időre ahelyett hogy szétoszlana a program futása közben, ahogy ez történik érték típusok esetén Sőt, nem is tudjuk kontrollálni, hogy ez az időigényes folyamat mikor történik meg.
Nem szeretném túlhangsúlyozni a Garbage Collector veszélyeit. Olyan modern nyelvek, mint a C# is nem véletlenül használják. Egy remek technológia ez, ami sokkal biztonságosabb és könnyebben kezelhető forráskódot eredményez. Egyszerűen meg szeretném jegyezni, hogy mindennek némi ára is van: és ez az időnként előforduló relatíve időigényes szemétgyűjtési folyamat.
Na de miért is mondtam mindezt el? Azért, hogy el tudjam magyarázni, mi értelme van annak, hogy létezik érték és referencia típus is, valamint hogy mikor melyiket szoktuk használni.
Miért létezik érték és referencia típus?
gondoljuk végig az eddig megtanultakat: Az érték- és a referenciatípusoknak is megvannak optimalizálás szempontjából az előnyei, de a hátrányai is.
- Mikor optimális a referencia típus? Egy érték típusú objektumot ha átadunk egy metódusnak vagy csak egy másik változónak, akkor a teljes objektumból egy másolat készül. Ez referencia típusok esetén sokkal egyszerűbb lehet. Csak az objektum referenciája került átírásra a változóban. Ez nagyobb adatot tartalmazó típus esetén nem csak gyorsabb, de kevesebb memóriát is használ el feleslegesen, hiszen nem tároljuk többszörösen az objektumot.
- Mikor optimális az érték típus? Azért vállaljuk sokszor ezt az egész objektumos másolást, mert ezáltal egy változó és a benne tárolt objektum kapcsolata kötött. A kettő egy az egyben megfeleltethető egymásnak. Emlékezzünk, az volt mindennek az előnye, hogy a memóriafelszabadításhoz nem szükséges a Garbage Collector, a változó megszűnésével törölhető az objektum is egyből. A másolgatások ezért nem is olyan rossz áldozatot jelentenek amikor a típus kevés adatot tartalmaz.
A C# logikája szerint ezt érdemesebb nem objektumonként hanem típusonként eldönteni, hisz egy típus tagváltozóinak összessége már meg is adja, mekkora lesz a típus objektumainak mérete. Ennek a hozzáállásnak egyéb előnye, hogy így egy változó típusából már következtetni lehet arra, hogy fog viselkedni, értékként vagy referenciaként.
Ennek oka, hogy sok egyéb viselkedésbeli különbség is következik abból, hogy egy típus érték vagy referencia, és ezen eltérő viselkedések miatt máshogy használjuk őket. Ezen különbségek egy részét már átnéztük, a többit pedig alább tárgyaljuk.
Ezen különbségekből következik, hogy ha el is tekintünk az optimalizációtól, akkor is előfordulhat, hogy egy esetben egy objektumnak a referencia szerinti hivatkozása a “kényelmesebb” máskor pedig az érték szerinti. Szerencsére a kényelmes és hatékony használat szempontjából is igaz az az általános szabály, hogy az egyszerű dolgok értékként kezelhetők jobban, a komplikáltabbak pedig referenciaként.
A null
és a default
A referenciatípusokra -Emlékezzünk!- tekinthetünk úgy, mint mutatókra, amik egy létező objektumra hivatkoznak. Ahogy azt előbb láttuk több változó mutathat egyazon objektumra is. Sőt az is lehetséges, hogy egy, ilyen referenciatípusú változó nem mutat semmilyen objektumra. Ekkor a változó értéke az úgy nevezett null
. A null
érték azt jelenti, hogy nem mutat a változó semmilyen referenciára: Nincs a változóban semmilyen objektum eltárolva. Ezt a speciális értéket egy érték típusú változó nem veheti fel.
Például mivel a string
, Transform
, GameObject
, a listák és a tömbök mindegyike, mind referencia típusúak, tehát mind felvehetik a null
értéket.
Transform t = null;
List<int> l = null;
GameObject go = null;
string s = null; // (Ez nem összekeverendő a 0 karakteres sztring-gel!)
string[] a = null; // (Ez nem összekeverendő a 0 elemű tömbbel!)
int i = null; // ⛔ERROR⛔ Érték típus nem veheti fel a null értéket
Minden típusnak van egy alapértelmezett vagy más néven default
értéke. Ez referencia típusok mindegyikére a null
, érték típusok esetén pedig mindig egyedileg meghatározott. Például az int
, float
valamint egyéb szám típusoknak a 0 a default
-ja, bool
-oknak pedig a false
.
Bármely típus alapértelmezett értéke elérhető a default
kulcsszóval:
// Referencia típusok
Transform t = default; // null
List<int> l = default; // null
GameObject go = default; // null
string s = default; // null
string[] a = default; // null
// Érték típusok
int i = default; // 0
float f = default; // 0f
bool b = default; // false
Vector2 v = default; // x = 0, y = 0
A null
-teszt
Ha nem vagyunk biztosak abban, hogy egy referencia változó értéke null
-e, akkor könnyedén ellenőrizhetjük a jól megszokott ==
(egyenlőségteszt) és !=
(egyenlőtlenségteszt) operátorokkal.
[SerializeField] Transform someComponent;
void Update()
{
if(someComponent != null) // Ha nem null a váéltozó értéke,
{
// Akkor kezdünk vele valamit:
Vector3 direction = (someComponent.position - transform.position).normalized;
transform.Translate(direction * Time.deltaTime);
}
}
Pár fontos megjegyzés a null
és a default
értékekkel kapcsolatban
- A
default
értéket kapja meg minden field azaz tagváltozó, ha nem állítjuk be külön. - Nem csak field-ek lehetnek
null
-ok. Lokális változók, függvények paraméterei és a visszatérésük is vehet felnull
értéket. - Az üres
string
és anull
nem ugyanaz. Egystring
változó felveheti anull
értéket, de tartalmazhatja az üres (azaz karaktereket nem tartalmazó)string
értéket is. - Az üres tömb és a
null
nem ugyanaz. Egy tömb változó felveheti anull
értéket, de lehet benne egy nulla elemű vagy nulla hosszú tömb is.
string s1 = null, s2 = ""; // s1 értéke nem egyezik s2-ével
int[] a1 = null, a2 = new int[0]; // a1 értéke nem egyezik a2-ével
- Egy
n
eleműT
típusú tömböt létrehozva:new T[n]
a tömb azT
típusdefault
értékeivel lesz tele. - A lokális változóknak előfordul, hogy nincsen semmilyen értéke még beállítva, azaz nincsenek definiálva. Ebben az esetben az ő értékük nem egyenlő
null
-lal, hanem ekkor a változó egyszerűen csak definiálatlan. - Léteznek modern C#-ban a
==
és a!=
operátorokon kívül is egyéb speciális esetekben használható egyszerűsített módjai is a nulltesztnek. Pár példa: - Fontos viszont, hogy a
UnityEngine.Object
-ből leszármazott osztályok objektumai, mint például aGameObject
,MonoBehaviour
, egyéb komponensek, aScriptableObject
és majd minden Unity specifikus osztály esetén a fenti??
??=
és?.
és egyéb speciális nullteszt operátorok nem használhatóak! Csakis a szimpla egyenlőségteszt (==
) és egyenlőtlenségteszt (!=
) operátorok garantálnak helyes működést. Tehát:
int[] a1 = new int[5]; // {0, 0, 0, 0, 0}
Transform[] a2 = new Transform[3]; // {null, null, null}
Érték és referencia típusú lokális változó is lehet definiálatlan, de csak referencia típusok lehetnek null
-ok.
int[] a = a1 ?? a2; // Ha az a1 változó tartalma nem null, akkor
// a változó értéke a1 lesz egyébként pedig a2
a ??= a1; // Csak akkor írjuk felül a értékét, ha az korábban null volt
list?.Sort(); // Csak akkor hajtjuk vége a Sort műveletet a list-en,
// ha az az nem null
someTransform?.Rotate(0,90, 0); // ⛔ HELYTELEN MEGOLDÁS
// Futási idejű hibát adhat!
if(someTransform!= null) // ✅ HELYES MEGOLDÁS
someTransform?.Rotate(0,90, 0);
Ennek mögöttes okát itt nem fejtem ki bővebben.
A ref
kulcsszó
Adat típusú paraméterek esetén is nyilatkozhatunk arról, hogy referenciaként történjen az átadás. Ekkor a metóduson belül, annak referenciaként megjelölt paraméterein történt módosítások “kihatnak” azokra a változók tartalmára is, amiket a híváskor adtunk meg.
Ha azt szeretnénk, hogy egy érték típusú paraméter referenciaként viselkedjen, akkor a ref
kulcsszót kell használni a metódus definiálásakor a megfelelő paraméter előtt. Emellett az out
-hoz hasonlóan ezt a kulcsszót ki kell írni híváskor is.
int n = 5;
DoubleOf(ref n); // Metódus hívása
Console.WriteLine(n); // 10 (Ez a ref kulcsszó használata nélkül 5 lenne)
// ...
void Double(ref int num) // Metódus definiálása
{
num *= 2;
}
Érték típusból referencia
Minden érték típusból könnyedén készíthetünk Referencia típust, ha beágyazzuk egy osztálya. Például alább elkészítettem az int típus referencia verzióját.
class Int
{
public int value; // Beágyazott érték
// Konstruktor
public Int(int value) => this.value = value;
// Implicit kasztok definiálása:
public static implicit operator int(Int i) => i.value;
public static implicit operator Int(int i) => new (i);
}
Azonban mind erre NINCS SZÜKSÉG, ugyanis a C# egyik beépített funkciója, hogy automatikusan legenerálja a fenti osztályt a háttérben. Ehhez csak annyit kell tennünk, hogy int?
-ként hívakozunk a típusra.
// Habár az int egy érték típus,
int? i = null; // az int? típusú változó felveheti a null-t mint érték
if (Random.value > 0.5) // 50% esély, hogy adunk-e a változónak egy konkrét értéket
{
int val = Random.Range(1, 11); // 1-10
i = val; // int kasztolása int?-ra automatikusan (implicit) működik
}
if (i.HasValue) // Ha van értéke: nem null
{
int val = i.Value; // int? kasztolása int-re automatikusan (implicit) működik
Debug.Log("Has value: " + val);
}
Egyenlőségvizsgálat
Vegyük a következő példát. Létrehozunk két-két objektumot egy referenciatípusból és egy értéktípusból. Mingkét esetben a két példányt ugyanazokkal az adatokkal töltjük fel, majd megvizsgáljuk, a két változó egyenlőségét az ==
operátorral.
// Dog Struktúra
Dog dog1 = new Dog { name = "Blöki", age = 5, speed = 12f, doesBite = false };
Dog dog2 = new Dog { name = "Blöki", age = 5, speed = 12f, doesBite = false };
// Goblin Osztály
Goblin goblin1 = new Dog { health = 100, isEnemy = true, damage = 5, range = 2};
Goblin goblin2 = new Dog { health = 100, isEnemy = true, damage = 5, range = 2};
Console.WriteLine(dog1 == dog2); // true
Console.WriteLine(goblin1 == goblin2); // false
Ebben az esetben négy különböző objektum létezik 2 Dog
és 2 Goblin
.
Az érték típusú változók vizsgálatánál csak a belső változók értékei lettek összehasonlítva és mivel azok megegyeztek, az eredmény igaz.
Ezzel szemben a referencia típusú változók vizsgálatánál azt ellenőrizte a C#, hogy a két változó ugyanarra az objektumra mutat-e. Ez hamis volt szóval az ==
vizsgálat is ezt adta vissza. Nem számított, hogy a két objektum belső értékei pontosan megegyeztek.
- Érték típus - Érték alapon történik összehasonlítás
- Referencia típus - Referencia alapon történik összehasonlítás
Hogy lehet akkor mégis két referencia típusú változó értéke egyenlő? Akkor, amikor két változóban ugyanaz az objektum szerepel, ami mint korábban láttuk referencia típusok eseténlehetséges. Például:
Goblin goblin2 = goblin1;
Console.WriteLine(goblin1 == goblin2); // true
A tömb és a Lista mint referenciatípus
A tömb és a lista egyaránt referenciatípus: Tömbök, Listák és a Foreach ciklus
Tehát:
- A
null
értéket fel tudja venni, és ez minden tömb és listadefault
értéke is. - A paraméterátadás minden tömb és lista esetén referencia szerint történik.
Ha egy tömböt vagy listát átadunk tehát egy metódus paramétereként és a metóduson belül módosítunk ezen tömbön vagy listán, akkor annak hatása metódus végrehajtása után is érzékelhető.
Például az alábbi ChangeArray
metódus “Eper”
-re állítja egy string
tömb minden elemét, anélkül, hogy lenne visszatérése és ez a változás a lefutás után érzékelhető.
string[] myStrings = { "Alma", "Körte", "Barack" }; // Minden tömb referenciatípus
ChangeArray(myStrings, "Eper"); // Átadjuk paraméterben a tömböt
Console.WriteLine(myStrings[1]); // Eper // A módosítás eredménye látszik
void ChangeArray(string[] array, string value) // Ez a metódus módosít a tömbön
{
for (int i = 0; i < array.Length; i++)
array[i] = value;
}
Mi a helyzet a string
-gel?
A string
-ek egy egyedi kategória. Ők kicsit kilógnak az eddigiek közül. Használatukban valahol az érték és a referencia típus közt állnak.
A string
-ek igazából a többi primitívtől eltérően referenciatípusok, szóval az értékátadás is midig referencia alapján történik. Ám egy string
objektum értéke nem változhat meg. Ezt úgy nevezzük, hogy a típus “immutable”. Ha bármit módosítunk, azzal egy új példányt hozunk létre.
Ennek az az eredménye, hogy bár a string
egy referenciatípus használat közben értéktípusnak érződik. Például ahogy egy metódus a paraméterben kapott int
értékét nem tudja úgy módosítani, hogy az kihasson a hívás pontján lévő változóra, ugyanígy egy paraméter string
-et sem lehet módosítani, úgy hogy az a metódushívás helyén észrevehető legyen.
Mivel a string
referencia típus felveheti a null értéket és ez a default
értéke is.
Itt megjegyzendő, hogy az üres string
és a null
nem ugyanaz. Egy string
változó felveheti a null értéket és a nulla hosszú, üres (azaz karaktereket nem tartalmazó) string
értéket is.
Az egyenlőségvizsgálat string
-ekre rendhagyó módon érték alapon történik. Tehát akkor is igazat fog mutatni ha nem ugyanarra az objektumra referál a két vizsgált változó, de az értékük egyezik.
Öröklés
Fontos tulajdonsága még az osztályoknak a struktúrákkal szemben, hogy képesek az öröklésre, ám ennek definiálásával itt még nem foglalkozunk. A téma később, az Objektum Orientált programozás-nál vesszük elő újra: Objektum orientáltság és öröklés (Félkész)
Rekordok
A string
-ekhez hasonlóan működnek a Record típusok is, ami az osztályok és a struktúrák mellett a 3. fajta összetett típus a C#-ban.
A rekordok is immutable értéktípusok, amiknél az egyenlőségvizsgálat érték alapon történik.