Developedia
Developedia
Referencia- és értéktípusok

Referencia- és értéktípusok

Vizsgáljuk meg a következő példát az összetett típusokkal foglalkozó korábbi Összetett típusok és példányaikÖ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.

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 a someInteger változónak.
  • … Dog típusú objektumot, Toto-t és azt értékül adtam a someDog változónak.

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
image
image
image

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:

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.
icon
Tehát durva egyszerűsítéssel élve kis objektumok akkor optimálisabbak, ha értékként viselkednek, nagy objektumok pedig ha referenciaként.

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 fel null értéket.
  • Az üres string és a null nem ugyanaz. Egy string változó felveheti a null értéket, de tartalmazhatja az üres (azaz karaktereket nem tartalmazó) string értéket is.
  • string s1 = null, s2 = "";       // s1 értéke nem egyezik s2-ével
    
  • Az üres tömb és a null nem ugyanaz. Egy tömb változó felveheti a null értéket, de lehet benne egy nulla elemű vagy nulla hosszú tömb is.
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 az T típus default értékeivel lesz tele.
  • int[] a1 = new int[5];                //  {0, 0, 0, 0, 0}
    Transform[] a2 = new Transform[3];    //  {null, null, null}
  • 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.
  • Érték és referencia típusú lokális változó is lehet definiálatlan, de csak referencia típusok lehetnek null-ok.

  • 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:
  • 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
  • Fontos viszont, hogy a UnityEngine.Object-ből leszármazott osztályok objektumai, mint például a GameObject, MonoBehaviour, egyéb komponensek, a ScriptableObject é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:
  • 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.

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.

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ökTömbök, Listák és a Foreach ciklusListák és a Foreach ciklus

Tehát:

  • A null értéket fel tudja venni, és ez minden tömb és lista default é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ő.

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)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.

Logo

Főoldal

Blog

Elmélet

3D Studio

Adatvédelmi nyilatkozat

GY.I.K.

Házirend

Szerző: Marosi Csaba / marosi.csaba@3d-studio.hu

DiscordGitHubLinkedIn
// 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.
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
// 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 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
                // 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);
}
// 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
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;
}