Képzeljük el a következő két osztályt mondjuk egy játékban!
class LivingObject // Sebezhető oszt.
{
public float health;
}
class Damager // Sebző osztály
{
public float damage;
}
A LivingObject
osztálynak van élete, a Damager
osztálynak van egy sebzés értéke.
Amikor két ilyen objektum valami módon “találkozik”, akkor a sebző levesz annyi életet a sebezhető osztálytól, amennyi a sebzése.
LivingObject livingObj = new LivingObject { health = 100 };
Damager damager = new Damager { damage = 100 };
// 3-szor megtörténik a "találkozás" és vele a sebzés:
livingObj.health -= damager.damage;
livingObj.health -= damager.damage;
livingObj.health -= damager.damage;
Képzeljük el, hogy a sebzés folyamata valamivel bonyolultabb, mint egy egyszerű kivonás. Pl.:
- Van egy Őselem nevű Enum a játékban: Tűz, Víz, Föld. Levegő:
enum Element { Water, Earth, Fire, Air }
- A
LivingObject
-nek és a Damager-nek is van egy Őselem field-je. LivingObject
-nek van egy rezisztenciája / ellenállása az a sajátjával azonos fajta őselem ellen.- Ha a
LivingObject
élete eléri a nullát, kiírja, hogy meghalt.
class LivingObject // Sebezhető oszt.
{
public float health;
public Element element;
public float resistance;
}
class Damager // Sebző osztály
{
public float damage;
public Element element;
}
Ebben az esetben egy kicsit bonyolultabb a sebzés művelete.
Ha a két objektum eleme nem egyezik akkor a teljes sebzés megtörténik. Ezzel szemben viszont, ha teszem azt, egy Tűz típusú Damager
támad meg egy tűz típusú LivingObject
-et, az kevesebbet fog sebezni, mintha bármelyik fél másik típusú lenne.
if (livingObj.element == damager.element)
{
livingObj.health -= damager.damage * (1 - livingObj.resistance);
}
else
{
livingObj.health -= damager.damage;
}
if (livingObj.health <= 0)
{
Console.WriteLine("Living Object Died");
}
Ez a művelet elég bonyolult ahhoz, hogy egy metódusba tegyük.
void Damage(LivingObject livingObj, Damager damager)
{
... Fenti sebzés algoritmus ...
}
LivingObject livingObj =
new LivingObject {health = 100, element = Element.Fire, resistance = 0.75f};
Damager damager =
new Damager {damage = 100, element = Element.Fire};
// 3-szor megtörténik a sebzés:
Damage(livingObj, damager);
Damage(livingObj, damager);
Damage(livingObj, damager);
A következő lépés megértéséhez képzeljük el, hogy sokkal nagyobb és összetettebb a kódbázisunk, mint amit eddig megírtunk soktíz osztály és sokszáz metódus van benne elosztva temérdek fájlban.
Egy valódi szoftveres projektre ezek a nagyságrendek még alacsonynak is számíthatnak.
Fejből kéne tudni mindenkinek azt, hogy létezik a Damage
metódus a többi sokszázzal egyetemben…. Mi lehet egy ennél jobb megoldás?
Típusokon értelmezett műveletek
Már talán hallottál arról, hogy a C# egy Objektum Orientált (OO) nyelv. Az objektum orientáltság elméletét és funkcióinak java részét nem fogjuk tudni ezen a kurzuson végig venni. Viszont fontos alapfogalmai az OO-nak az osztályok (általánosabban összetett típusok) és példányaik, az objektumok.
Tehát egy összetett típus nem csak az őt felépítő field-ek összessége jellemez, hanem a rajta végezhető műveletek is.
Ennek tükrében vigyük tovább a fenti példát és a Damage
metódust helyezzük bele a LivingObject
osztályba ezúttal a TakeDamage
néven:
class LivingObject // Élettel (health) rendelkező és sebezhető osztály
{
public float health;
public Element element;
public float resistance;
public void TakeDamage(Damager damager)
{
if (element == damager.element)
{
health -= damager.damage * (1 - resistance);
}
else
{
health -= damager.damage;
}
if (health <= 0)
{
Console.WriteLine("Living Object Died");
}
}
}
(Ne csak a field-ek de a metódusok elé is írjuk ki a public kulcsszót! Később tanulni fogjuk, miért: Tagváltozók és -metódusok láthatósága )
Ebben az esetben nem volt szükség arra, hogy paraméternek átadjunk egy LivingObject
objektumot is. Hiszen a metódus pont az adott LivingObject
fieldjeit fogja felhasználni.
Kívülről ezután a sebzés így nézne ki:
LivingObject livingObj =
new LivingObject {health = 100, element = Element.Fire, resistance = 0.75f};
Damager damager =
new Damager {damage = 100, element = Element.Fire};
// 3-szor megtörténik a sebzés:
livingObj.TakeDamage(damager);
livingObj.TakeDamage(damager);
livingObj.TakeDamage(damager);
Ennek kicsit más a logikája mint az eddigi metódusainknak:
Most azt mondtuk, hogy van egy típusunk a LivingObject
és van egy rajta értelmezett művelet a TakeDamage
. Tehát a LivingObject
típusú objektumunkon hajtunk végre metódust.
Egy típus műveleteihez a .
(pont) operátorral férünk hozzá. Ezáltal, ha leírjuk egy változó nevét és ütünk egy .
(pont) karaktert, az IDE kódkiegészítője fel fogja ajánlani, az összes field-et és az összes műveletet, ami az adott típuson végezhető. Ez nagy-nagy segítséget jelent a programozásban.
A típuson értelmezett metódusokat más néven tagmetódusnak nevezzük.
Eddig az osztály metódusai technikailag nem különböznek attól, mint mikor egy teljesen független metódusban hajtottuk végre az algoritmust. Csupán egy másik módja a kód szervezésének, ami főleg hosszútávon nagyban segíti az átláthatóságot. Viszont hamarosan megismerünk olyan programozási koncepciókat, amivel még több jelentősége lesz annak, hogy osztályon belül helyezünk el egy metódus vagy sem: Tagváltozók és -metódusok láthatósága.
Pár elképzelhető példa
Ha van egy …… osztályunk, annak lehet …… metódusa.
class Zár
→void Kinyit(Kulcs kulcs)
ésvoid Bezár(Kulcs kulcs)
class Lövedék
→void Kilő(Pozíció honnnan, Irány merre)
class Háziállatt
→void Megetet(Étel étel)
ésvoid Sétáltat()
(Ha pl. tamagotchi-t írunk)class Inventory
→ elem hozzáadás:void Add(Item item)
és kivétel:void Remove(Item item)
class FPSPlayer
→ ugrás:void Jump()
és lövés:void Shoot()
Próbáljatok ti is kitalálni hasonlókat.
Operator Overolading
Egy osztálynak tetszőleges számú metódusa lehet különböző névvel.
Egyazon nevű metódusokból is lehet több, ha különbözik ezek paraméterlistája. Az, hogy egy osztálynak több azonos nevű művelete van az azt mutatja, hogy ugyanarra a dologra való mindegyik parancs, de más bemenetekkel működik.
Például vegyük TakeDamage(Damager)
metódust, ez egy Damager típusú objektumot vár paraméterként. Elképzelhető azonban, hogy más is le tud venni életet a LivingObject
-től.
Ebben az esetben lehetne egy olyan változata ennek a metódusnak, ami egy float
sebzésmennyiség értéket és egy Element
enum típusú értéket vár paraméterben:
public void TakeDamage(Damager damager) // Eredeti metódus
{
if (element == damager.element)
health -= damager.damage * (1 - resistance);
else
health -= damager.damage;
if (health <= 0)
Console.WriteLine("Living Object Died");
}
public void TakeDamage(flaot damage, Element element) // Metódus egyazon néven
{
if (element == element)
health -= damage * (1 - resistance);
else
health -= damage;
if (health <= 0)
Console.WriteLine("Living Object Died");
}
A fenti kód helyes és működőképes. Egy osztályon belül lehet bárhány azonos nevű metódus, ha a paraméterlistája különbözik. Ezentúl két metódussal is elérhető ugyanaz a művelet, csak más bemenettel.
Ezt nevezzük operátor overloading-nak vagy nem túl széles körben használt magyar kifejezéssel élve operátor túltöltésnek.
Akkor engedélyezett az overload, ha a metódusok paraméterlistájának típusai és sorrendje alapján egyértelműen megállapítható a fordító számára, hogy melyik verziót kell hívnia.
A fenti példában azonban van még egy nagy szépséghiba: Nagy mennyiségű kódismétlés. Ha módosítani szeretnénk a TakeDamage
működésén, akkor már két helyen kell megtennünk és, ha elfelejtjük az egyiken, az hibás működést fog eredményezni, méghozzá a nehezebben felderíthető fajtából.
Gondolkodj el rajta, hogy oldanád meg ezt a problémát!
A konstruktor
Egyedi metódusokat is definiálhatunk egy objektum létrehozására. Ezeket nevezzük konstruktoroknak.
Egy konstruktornak nincs külön típusa és neve, helyette mindig az osztály nevével jelezzük, hogy konstruktort írunk.
class LivingObject
{
public float health;
public Element element;
public float resistance;
public LivingObject(float h, Element e, float r)
{
health = h;
element = e;
resistance = r;
}
}
Ebben az esetben a LivingObject
egy egyszerűsített módon is létrehozható.
// Konstruktor nélkül:
LivingObject livingObj1 =
new LivingObject {health = 100, element = Element.Fire, resistance = 0.75f};
// Konstruktorral:
LivingObject livingObj2 = new LivingObject (100, Element.Fire, 0.75f);
Konstruktorral extra logikát is hozzáadhatunk egy objektum létrehozásához. Például a lenti példában két érték közé szorítom be a resistance kezdeti értékét. Más szavakkal: nem engedem, hogy a resistance
kisebb legyen mint 0 vagy nagyobb, mint 1.
class LivingObject
{
public float health;
public Element element;
public float resistance;
public LivingObject(float h, Element e, float r)
{
health = h;
element = e;
if(r < 0)
r = 0;
if(r > 1)
r = 1;
resistance = r;
}
}
Ezáltal nem tud valaki hibásan létrehozni egy LivingObject
-et mondjuk 1-nél nagyobb resistance
-szel.
ToString()
Korábban tárgyaltuk, hogy Minden objektumból lehetséges string-et készíteni.
Összetett típusok esetén alapesetben ez a metódus csak a típust adja vissza szöveges formában, azonban lehetőségünk van egyedi értéket adni, ha megvalósítjuk a ToString()
metódust a következő fejléccel:
public override string ToString()
{
...
}
Példa a típuson belüli megvalósításra:
class Dog // Struct is lehetne
{
public string name; // Neve
public int age; // Kora
public float speed; // Sebessége
public bool doesBite; // Harap-e?
public override string ToString()
{
return $"Name: {name}, Age: {age}, Speed: {speed.ToString("0.0")}";
}
}
Használat:
Dog blöki = new Dog { name = "Blöki", age = 5, speed = 12, doesBite = false };
Dog fifi = new Dog { name = "Fifi", age = 3, speed = 10.5f, doesBite = true };
string blökiText = blöki.ToString(); // Eredmény: "Name: Blöki, Age: 5, Speed: 12.0"
string fifiText = fifi.ToString(); // Eredmény: "Name: Fifi, Age: 3, Speed: 10.5"
A fentiek bővabben a kövekező fejezeteketekben lesznek tárgyalva: Objektum orientáltság és öröklés (Félkész), Absztrakció, Polimorfizmus (Hamarosan)