fel
le

Az öröklődés

Az öröklődés során egy objektumosztály megjelöl egy másik, már létező osztályt őseként. E miatt az ős osztályban definiált minden mezőt, metódust, konstanst, property-t örökli, mintha az eleve a részét képezné.

Ezen technikát használjuk, ha az új objektumosztály fejlesztése során nem kívánunk mindent előlről, a nulláról elkezdeni. Helyette keresünk egy olyan másik osztályt, amelyik már készen van, le van tesztelve, hasonló feladat kezelésére íródott mint a mi új feladatunk. Amennyiben találunk ilyet, úgy az öröklés segítségével átvesszük tőle a már kész dolgokat, és csak a hiányzó, vagy számunkra nem megfelelő működésű részek kidolgozására kell koncentrálnunk.

Minél megfelelőbb osztályt választunk ősnek, annál kevesebb fejlesztési feladat marad ránk. Hagyatkozzunk ebben a Base Class Library hatalmas osztálygyűjteményére. Annak a valószínűsége, hogy olyan osztály fejlesztésére szorulunk, amelyhez hasonló sincs sehol - elég kicsi.

Amennyiben nem ismerjük az öröklődés lehetőségét, úgy a szokásos megoldási lehetőségünk, hogy vesszük a kívánt ős osztály forráskódját, és copy-paste módszerrel átemeljük azt a forráskódunkba, majd elkezdjük a kódot módosítani.

Ennek több hátránya is van:

  • a copy-paste által átemelt kódban szereplő, nem módosított metódusok két példányban is belekerülnek a futtatható programunkba, megnövelve ezzel annak hoszzát.
  • a copy-paste után elveszítjük a kapcsolatot az eredeti forráskód és a mi módosított forráskódunk között. Amennyiben az eredeti kódban annak programozója javításokat végezne el, azon módosítások nem vezetődnek át automatikusan hozzánk. Újra meg kell ismételni a copy-paste lépést, és újra végigvezetnünk azon a mi módosításainkat. A gyakorlati életben ez több veszéllyel jár, mint haszonnal. A végén nemhogy időt nyernénk, hanem inkább veszítünk ezzel a módszerrel.
  • nagyon gyakori az, hogy nem áll rendelkezésre a forráskódja az ős osztálynak (mint pl. a Base Class Library osztályainak sem ismert a forráskódja), ezért ezen módszer egyszerűen nem is használható.

Az öröklődés deklarálása

Az öröklődést a saját fejlesztésű osztály deklarációja során kell feltüntetni, az ős-osztály nevét kettőspont mögött kell megadnunk:

class TRadiosMagno
{
  public Kazetta kazetta = null;
  public void Start() { ... }
}

class THaziMozi:TRadiosMagno
{
  public Hangfal[] hangfalak = new Hangfal[5];
}
 

A fenti példában szereplő THaziMozi osztály őse a TRadiosMagno osztály.

Egyszerre csak egy őst választhatunk, ezért alaposan fontoljuk meg a választásunk. Ugyanakkor egy osztály több másiknak is lehet őse. E miatt öröklődési fáról beszélhetünk, melyben a legfelül a választott kiinduló osztály foglal helyet, az alatta lévő szinten a közvetlen leszármazott osztályok, majd annak a gyermek-osztályai, stb.

Amennyiben nem választunk őst (nem jelölünk ki explicit módon egyet sem), úgy a fordító ezt úgy értelmezi, hogy az Object nevű objektumosztályt választjuk ősnek.

Tehát olyan osztályt fejleszteni, amelynek nincs őse - lehetetlen. A C#-ban csak egyetlen ilyen osztály létezik, maga az Object.

Az előző öröklődési fát újra végiggondolva tehát az öröklődési fa kezdőpontja maga az Object osztály, és a belőle kiinduló fejlesztési ágakon haladva minden más osztályhoz el lehet jutni valahány lépésben.

Mit is öröklünk?

Amennyiben ősnek jelölünk meg egy kiválasztott másik osztályt, úgy a legegyszerűbb ha azt képzeljük el, mintha a fent említett copy-paste lépést maga a fordítóprogram végezné el helyettünk automatikusan. Ez azt jelenti, hogy az ős osztályból válogatás nélkül öröklünk mindent. Minden mezőt, konstanst, property-t, metódust. Ezek eleve részét képezik az osztályunknak. A továbbiakban természetesen lehetőségünk van újabb mezőket, metódusokat hozzáadnunk az osztályunkhoz, hogy növeljük annak képességeit.

Az ős-osztálytól örököljük annak public és protected mezőit és metódusait. Ezen mezőkre a saját metódusainkban hivatkozhatunk, az örökölt metódusokat meghívhatjuk.

Hasonlóan örököljük a private mezőket és metódusokat is, de azok védelmi szintje, hatásköre miatt ezekre a saját fejlesztésű metódusokban nem hivatkozhatunk közvetlenül.

A private védelmi szintű részeket is örököljük, de ezekre csak indirekt módon tudunk hivatkozni. Ez az első különbség az öröklés, és a copy-paste között. Copy-paste esetén a private részek fizikailag is átkerülnének az új osztályba, és a private hatáskör ekkor erre az új osztályra vonatkozna már.

Nyilván felmerül a kérdés: akkor ezeknek milyen hasznát vehetjük? Nos, a válasz egyszerű: meghívhatunk olyan örökölt protected vagy public metódust, amely kódja az ős-osztályban helyezkedik el, és ezen metódus hivatkozhat a private mezőkre és metódusokra. Tehát bár közvetlenül, direkt módon nem vehetjük ezen private részek hasznát, de közvetve, indirekt módon igen.

class TAlapMobilTelefon
{
   private int PIN_Kod;
   public bool PIN_Ellenorzes( int kod )
   {
      if (kod == PIN_Kod) return true;
      else return false;
   }
}
class TOkosTelefon:TAlapMobilTelefon
{
   public void Bekapcsolas()
   {
     for(int i=0;i<3;i++)
     {
        int x = PinKodBekeres();
        if (PIN_Ellenorzes(x))
        {
           TelefonInditas();
           return;
        }
     }
     TelefonZarolasa();
   }
}
 

Mivel a private részek elérhetősége problémás a gyermekosztályban, ezért a tervezés során fokozottan fontoljuk meg, hogy az adott mezőt vagy metódust valóban private védelmi szintűvé kívánjuk-e alakítani, vagy a protected is elégséges lenne! Csak valóban indokolt esetben tegyünk mezőt vagy metódust private-a!

A private metódusok jellemző szerepe az osztály tervezése közben az, hogy valamely segédfunkciót lát el. Ezen private metódust csakis az adott osztály másik metódusából hívható meg. E miatt ezen private metódus belsejében ritkán van szükség a paraméter-, és egyéb értékek ellenőrzésére, hiszen ezt az őt meghívó metódus már általában megtette.

A fenti private metódus ugyanakkor protected-é alakítása nem veszélytelen, hiszen ekkor azt a gyermekosztály is felhasználhatná, de mivel a private metódus már nem ellenőrzi le esetleg az adatokat, így ez könnyen működési zavart válthat ki, az osztály nem tartatja be teljes körűen a védelmet. Ebben az esetben az osztály tervezője a hozzáférést letilthatja a private védelmi szint alkalmazásával.

Új mezők bevezetése

Osztály készítése során mezőket és metódusokat készítünk. Az öröklődés során az ős osztálytól öröklünk mezőket, de van lehetőségünk újabb mezőkkel kiegészítenünk a gyerekosztályunkat. Ennek során felmerülhet olyan jellegű probléma, hogy egy, az általunk elkészítendő új mező neve megegyezik valamely, az ős osztályban deklarált, örökölt mezőjével (ez egyébként szinte biztosan tervezési hibára utal, és bár a C# nyelvben szintaktikailag van lehetőség a problémát megoldani, de a szituáció kerülendő!).

Ilyen szituációban a hatáskörök elemzése nyújt választ a problémás esetekben.

class TElso
{
   private int a;
   protected int b;
   public int c;
}

class TMasodik:TElso
{
   public string a;
   public double b;
   public string c;
   public string d;
}
 

A fenti példában az ős osztályunk mindhárom mezőjét örökli a szóban forgó gyermekosztály, aki egyező nevű mezők bevezetését szeretné elvégezni.

Az a mező esetén egyező nevű új mező létrehozásának nincs akadálya, hiszen ezen a mező az ős osztályban private hatáskörű. Ennek megfelelően hatásköre nem terjed túl a TElso osztály keretein, így a TMasodik-beli egyezű nevű mezővel nem kerül ütközésbe.

A b nevű mező bevezetésével (és hasonlóan a c mezővel is) azonban problémák vannak. Ugyanis ezen örökölt mezők elérhetőek lennének a TMasodik osztály metódusaiban is, hiszen hatáskörük kiterjed a gyermek-osztályra is. Az újonnan bevezetett egyező nevű mezők hatásköre átfedi ezt a területet. Ilyen esetek mindíg problémásak, hiszen a mezőre hivatkozás nem lesz egyértelmű:

class TMasodik:TElso
{
   public string a;
   public double b;
   public string c;
   public string d;

   public void duplazas()
   {
     b = b*2;
   }
}
 

A duplazas() metódus belsejében lévő értékadó utasítás esetén el szabad bizonytalanodni, hogy melyik b mezőre is gondolunk. Mindkét b mező esetén (az int és a double esetén is) értelmes lenne a kód, vajon melyik mező értékét fogja ez megduplázni?

A hatáskör-ütközések minden programozási nyelvben előforduló jelenségek - de minden esetben kerülendők. Mindíg problémát okoznak, és mindíg tévesztésre - e miatt hibázásra adhatnak okot. Csak erősen indokolt esetben alkalmazzuk, és olyan környezetben, ahol nem keveredésről, hanem elfedésről van szó. Ebben az esetben a korábbi azonosítóról lemondunk, csak az újradefiniált (szűkebb) környezetbelit kívánjuk használni.

Az ilyen szituációkat a fordítóprogram érhető okokból nem szereti. A tapasztalatok szerint az ilyen esetek a programozókat is megzavarják, gyakori és nehezen felderíthető programhibák megelágyai. Ugyanakkor nagyon ritkán indokolt az az eset, hogy az újonnan bevezetendő mező neve meg kell egyezzen az örökölt mező nevével. Egyszerűbb lenne az új mezőnek eltérő nevet választani, hiszen a névválasztás a programozó feladata.

Mivel valójában nem okoz nehézséget egyszerűen más nevet választani, az egyező név pedig csak a bajt okozná, ezért a C# fordító egyszerűen nem fogadja el a fenti problémás esetet, és szintaktikai hibára hivatkozva megtagadja a TMasodik osztály kódjának lefordítását. Ezzel kényszeríti a programozót, hogy gondolja át a névválasztását, és inkább egy eltérő, egyedi nevet válasszon az újonnan bevezetett mezők esetén.

class TMasodik:TElso
{
   public string a;
   public double bb;
   public string cc;
   public string d;

   public void duplazas()
   {
     bb = bb*2;
   }
}
 

Ez a kényszer nem vonatkozik az a mezőre (akinél nincs hatáskör-ütközés), és a d mezőre, melynél szintúgy nincsen.

Ugyanakkor a programozónak van döntési joga ez ügyben. Dönthet úgy, hogy nem hajlandó másik nevet választani a saját mezőinek, inkább vállalja az ezzel járó kényelmetlenségeket, és nehézségeket. Ekkor meg kell jelölni a problémás mezőket a new kulcsszóval (jelentése új).

class THarmadik:TElso
{
   public string a;
   new public double b;
   new public string c;
   public string d;

   public void duplazas()
   {
     b = b*2;
   }
}
 

A new kulcsszót csak ilyen jellegű, problémás esetben szabad használni. Az a és d mezők esetén ezen kulcsszók használata tilos lenne.

A new kulcsszó felhívja a forráskódot böngésző programozók figyelmét arra, hogy ezen mezők újradeklarálásra kerültek, ezért óvatosan értelmezzék ezzel kapcsolatosan a kódot. A fordítóprogram felé ezen new kulcsszó azt a jelzést közvetíti, hogy igen, tudok róla hogy ilyen mező már létezne, vagyis ezen kulcsszó mintegy megnyugtatja a fordítóprogramot, hogy a programozó átgondolta, tudja mit csinál, és továbbra is kitart döntése mellett a mezők névválasztásával kapcsolatosan.

Ilyen megerősített esetekben a fordítóprogram tudomásul veszi a hatáskörök átfedésével kapcsolatos problémákat. Átfedés esetén mindíg az az azonosító élvez elsőbbséget, amely később került deklarálásra, aki hozzánk közelebb van, vagyis elsősorban a saját, új mezőink:

class THarmadik:TElso
{
   new public double b;

   public void duplazas()
   {
     b = b*2;
   }
}
 

A fenti kódban például a b mező a new-al megjelölt, új, double típusú mezőt jelenti, annak az értékét fogja a függvény megduplázni.

class TNegyedik:THarmadik
{
   public void triplazas()
   {
     b = b*3;
   }
}
 

A TNegyedik osztályban már újabb b nevű mező nem került bevezetésre, de az öröklődés révén mind a TElso-beli int, és a THarmadik-beli double típusú b mezőt örökölte. A triplazas() során újra előjön az a kérdés, hogy ez melyik b mező lesz.

Szeretném megragadni az alkalmat, hogy újra elmondjam: az hogy egy mezőt újradeklarálunk, az folyamatos problémát fog okozni. A THarmadik programozója vállalta ezt a következményt, úgy döntött, ezt ő tudja és hajlandó kezelni. Csakhogy mint látjuk, a problémát öröklik a további gyermekosztályok is, akik mit sem tehetnek erről a dologról. A THarmadik osztályt készítő programozó felelőssége így természetesen megnő ez ügyben.

Visszatérve a problémára - a triplazas() függvényben szereplő b azonosító az újonnan bevezett double típusú mezőt fogja jelölni, hiszen annak hatásköre a bevezetési ponttól számítva elfedi a régebbi b mezőt. Újra csak előjön a hozzánk közelebbi deklaráció ereje.

Amennyiben mégis szeretnénk az immár elfedett, elrejtett mezőre hivatkozni, ahhoz trükkökre lesz szükség:

class TNegyedik:THarmadik
{
   public int oszto_e( int x)
   {
     if ( (this as TElso).b % x ==0 ) return true;
     else return false;
   }
}
 

A this as TElso két trükköt is tartalmaz. A this kulcsszóval hivatkozhatunk a híváskori példányra, akit az as operátorral típusmódosítunk a TElso típusra. Ekkor mintegy visszautazunk az időben, amikor még csak a TElso típusunk volt, és abban a b mező, az int típusú mező létezett csak. Így tudjuk elérni az örökölt, ámde már elfedett b mezőt újra.

Ezen trükk nem működik osztályszintű mezők, osztályszintű metódusok esetén, mivel ezen esetekben nem használható a this kulcsszó. Osztályszintű mezők és az öröklődés

Osztályszinten a mezők öröklődése nem ugyanezeken az elveken működik.

class TOsztElso
{
   public static int a;
}

class TOsztMasodik:TOsztElso
{
   public static int b;
}
 

A fenti kis példában az a gondolatunk támadhat, hogy a TOsztMasodik osztálynak két darab osztályszintű mezője van, az örökölt a és az újonnan bevezetett b. Ezt a hitet erősíteni látszik az a tény, hogy az alábbi főprogram szintaktikailag működőképes:

public static void Main()
{
   TOsztElso.a = 1;
   TOsztMasodik.a = 2;
   TOsztMasodik.b = 3;
}
 

Igen ám, de ha alaposabban megkaparjuk a fenti kódot, és a főprogram végén kiírjuk az osztályszintű mezőkbe elhelyezett értékeket, akkor alaposan meg fogunk lepődni:

public static void Main()
{
   ...
   Console.WriteLine("TOsztElso.a = {0}",TOsztElso.a); // 2
   Console.WriteLine("TOsztMasodik.a = {0}",TOsztMasodik.a); // 2
   Console.WriteLine("TOsztMasodik.b = {0}",TOsztMasodik.b); // 3
}
 

Vagyis azt láthatjuk, hogy valójában nem örököltük az a mezőt a TosztElso osztálytól, mindössze mostmár erre a mezőre a TOsztMasodik.a néven keresztül is lehet hivatkozni. Mindkét hivatkozás ugyanarra, a fizikailag csak egy példányban létező a mezőre mutat, így fordulhat elő, hogy a TOsztElso.a mező értéke a kiíráskor 2 lesz.

Amennyiben azt szeretnénk, hogy a gyermekosztályban saját a mező legyen, független memóriaterületen, úgy ebben a new kulcsszó tud segíteni:

class TOsztElso
{
   public static int a;
}

class TOsztMasodik:TOsztElso
{
   new public static int a;
}

public static void Main()
{
   TOsztElso.a = 1;
   TOsztMasodik.a = 2;
   Console.WriteLine("TOsztElso.a = {0}",TOsztElso.a); // 1
   Console.WriteLine("TOsztMasodik.a = {0}",TOsztMasodik.a); // 2
}
 

Amennyiben az újradeklaráció, és a hatáskörátfedés miatti helyzetet kívánjuk kezelni egy metódus belsejében, akkor a kívánt mező eléréséhez sem a this, sem az as kulcsszavas trükkökre nincs szükség. Egyszerűen meg kell nevezni, melyik osztálybeli statikus mezőre gondolunk:

class TOsztElso
{
   public static int a;
}

class TOsztMasodik:TOsztElso
{
   new public static int a;

   public static void akarmi()
   {
     TOsztElso.a = 1;
     a = 2;
   }
}
 

A fenti akarmi() függvényben az a azonosító az új, frissen újradeklarált mezőt jelöli. Az ős osztálybeli elfedett a mező eléréséhez egyszerűen annak eredeti nevét, TOsztElso.a-t kell használnunk.

Hernyák Zoltán
2013-03-17 19:06:15