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

Az öröklés során az örökölt metódusok sok esetben (egyszerűbb esetekben) úgy viselkednek, mintha azok eleve osztályunk részét képeznék. Ugyanúgy meghívhatóak, mintha eleve az osztályunkban lett volna a kód megírva.

Természetesen gond van a 'private' hatáskörű metódusokkal, amelyek nem hívhatóak meg a gyermekosztályunk példányán keresztül sem, illetve a gyermekosztályban bevezetett új metódusokból sem lehet őket meghívni, mivel hatáskörük nem terjed ki a gyermekosztályra.

Az öröklődést a fordító az általa ismert szabályok szerint kezeli. Ezen szabályok ismerete azért kulcsfontosságú, mert ez a legerőteljesebb OOP-s technika. Aki ezzel a technikával jól bánik, az tudja igazán kihasználni a benne rejlő lehetőségeket. Aki rosszul bánik ezzel a technikával, az könnyedén képes hibásan működő kódot írni, amelyben rejlő hibák nehezen kideríthetőek.

Az öröklődés működése

Az alábbi példában bevezetünk egy TAuto nevű objektumosztályt, melyben néhány alap mező, és metódus kerül deklarálásra:

class TAuto
{
   public double tankMaxMerete;
   public double benzinMennyiseg;
   public double betankol(double mennyiseg)
   {
      if (benzinMennyiseg+mennyiseg<=tankMerete)
         benzinMennyiseg += mennyiseg;
      else
         throw new Exception("Ennyi benzint nem bír befogadni a tank!");
   }
   public bool tankUres()
   {
     return (benzinMennyiseg == 0);
   }
}
 

Ebből a TAuto-ból továbbfejlesztünk egy új osztályt, a TSportAuto-t, amelyet már be is lehet indítani, és le is lehet állítani:

class TSportAuto:TAuto
{
   protected bool motorMegy = false;
   public void Beindit()
   {
     if (motorMegy)  throw new Exception("Nem illik jaro motorra rainditani!");
     if (tankUres()) throw new Exception("Elotte talan tankolni kellene!");
     motorMegy = true;
   }
   public void Leallit()
   {
     motorMegy = false;
   }
}
 

A TSportAuto örökli a TAuto-beli mezőket és metódusokat. Az alábbi főprogram mutatja, hogy ezen metódusok és mezők ugyanolyan szintaktikával használhatóak, mintha azokat eleve a TSportAuto osztályban írtuk volna meg:

class FoProgram
{
  public static void Main()
  {
     TSportAuto x = new TSportAuto();
     x.tankMerete = 55;
     x.benzinMennyiseg = 0;
     x.betankol( 30 );
     x.Beindit();
     x.Leallit();
  }
}
 

Újonnan bevezetett metódusokból örökölt metódusok meghívása

A fenti példában is látszik, hogy bár a TSportAuto direktben nem tartalmaz betankol() metódust, de az öröklődés során ezt átveszi az ősétől. E miatt egy TSportAuto példányon keresztül akár meg is hívható. Ugyanakkor az új metódusokból (pl. a Beindit()-ból) egy örökölt metódus meghívható (mint a tankUres()-el is történik).

Ezzel a szituációval általában nincs gond. Nem jellemző, hogy egy örökölt metódus meghívása vitatott esetet eredményezne.

OVERLOADING

Az overloading szabály azt mondja ki, hogy ugyanazon névvel több metódus is létezhet, de a formális paraméterlistájuk egyértelműen különböző kell legyen. Ezen különbség azért fontos, hogy a hívás helyén feltüntetett aktuális paraméterlista alapján a fordítóprogram egyértelműen el tudja dönteni, melyik metódus-váltotatot kívánjuk meghívni.

Ha ezen döntést nem tudja egyértelműen meghozni a fordító, akkor fordítási hibát jelez, és kér bennünket, hogy adjunk meg több információt az aktuális paraméterlistában, amely egyértelműsíti a helyzetet. Ez általában explicit típuskonverziót jelent a kritikus paraméterek esetén.

class TMatematika
{
   public int max( int a, int b )
   {
     if (a>b) return a;
     else return b;
   }
   public double max( double a, double b )
   {
     if (a>b) return a;
     else return b;
   }
}
 

Ebben a két max() függvények között van különbség, az egyik int-ekkel, a másik double-kkel dolgozik. A hívás helyén megjelölt értékek alapján a hívás "célja", a megfelelő max függvényváltozat egyértelműen kiválasztható:

class FoProgram
{
   public static void Main()
   {
     Console.WriteLine("maximum={0}", max(2,3) );
   }
}
 

Nem ennyire egyértelmű a helyzet az alábbi esetekben:

class TMatematika
{
    public short szoroz(double a, short b)
    {
         return (short)(b*a);
    }
    public ushort szoroz(float a, ushort b)
    {
        return (ushort)(b*a);
    }
}
 

Az alábbi konkrét esetben a függvény-változat nem egyértelműen választható ki:

class FoProgram
{
  public static void Main()
  {
     TMatematika x = new TMatematika();
     int k = x.szoroz(2,3);
  }
}
 

De némi rásegítéssel a választás egyértelműsíthető:

class FoProgram
{
  public static void Main()
  {
     TMatematika x = new TMatematika();
     int k = x.szoroz( (double)2,3);
  }
}
 

Új metódusok bevezetése OVERLOADING mellett

A gyermekosztályban természetesen van lehetőség új metódusokkal kiegészíteni az osztályt. Amennyiben ezen új metódusok neve nem új - ilyen néven már van metódus örökölve, de az új metódus paraméterlistája különböző, úgy az OVERLOADING szabály mellett ennek azért nincs semmi akadálya, mert az aktuális paraméterlista alapján egyértelműen eldönthető, hogy az örökölt metódust kívánjuk aktualizálni, vagy az újonnan létrehozottat:

class THallgato
{
   public void Beiratkozas()
   {
     ...
   }
}

class THallgato2:THallgato
{
  public void Beiratkozas( int ev, int felev )
  {
    ...
  }
}
 

A fenti esetben a Hallgato2 osztály fel van készítve arra, hogy beiratkozzon az aktuális félévbe az örökölt paraméter nélküli Beiratkozas() metódus segítségével, de adott esetben konkrét év és félévre beiratkozni is képes az újonnan fejlesztett metódus segítségével.

class FoProgram
{
  public static void Main()
  {
     THallgato2 k = new THallgato2();
     // örökölt metódus meghívása
     k.Beiratkozas();
     // új metódus meghívása
     k.Beiratkozas(2006,1);
  }
}
 

Új metódusok bevezetése OVERLOADING hiányában

Amennyiben a gyermekosztályban bevezetett metódus neve és paraméterezése megegyezik az ős osztályban deklarált változattal, akkor problémák lépnek fel. Ez esetben az új metódus "elfedi" az örökölt metódust, mivel a két azonosító (metódusnév) ugyanazt a típust képviseli. A hívás helyén megadott aktuális paraméterlista nem segít a döntéshez, hogy melyik változat hívódjon meg, hiszen az mindkettőre ráillik.

Ebben az esetben csak a szabály ismerete segít. A szabály, hogy hatáskörátfedés esetén a gyermekosztályban lévő deklaráció eltakarja az ős osztálybeli deklarációt. Ez veszélyes művelet, ezért a fordítóprogram "megerősítést" kér, hogy valóban ezt akarjuk-e. Ez a megerősítés ugyanazon módon kell megadni, mint mezők esetén: a 'new' kulcsszó segítségével:

class TAuto
{
   protected bool motorMegy = false;
   public void Beindit()
   {
     if (motorMegy) throw new Exception("Nem illik jaro motorra rainditani!");
     motorMegy = true;
   }
}

class TOkosAuto:TAuto
{
  new public void Beindit()
  {
    if (motorMegy) return;
    base.Beindit();
  }
}
 

A fenti esetben az új osztályban deklarált új metódus-változat esetén a new kulcsszó használata nem kötelező, de elnyomja a fordítás során fellépő figyelmeztető (warning) üzenetet.

A base.Beindit() az ős-osztályban (TAuto) deklarált Beindit() metódus meghívását jelenti. Ez gyakori az ilyen esetekben, amikor az újonnan deklarált metódusban elvégezzük a módosítás miatti lépéseket, majd végrehajtjuk a régi kódot. Ez a fenti esetben csak akkor történik meg, ha a motor még nem megy. Így kikötöttük azt azt esetet, amikor járó motor esetén a régi kód kivételt dobott, ez mostmár nem következhet be, vagyis a fejleszett autó "kivédi" a ráindítást.

Amikor meghívjuk valamelyik metódus-változatot, a fordítóprogram a példány típusából következtet arra, hogy melyiket is kívánjuk aktivizálni.

class FoProgram
{
  public static void Main()
  {
     Tauto x = new TAuto();
     x.Beindit();
     
     TOkosAuto z = new TOkosAuto();
     z.Beindit();
  }
}
 

A fenti példában az x.Beindit() esetében a régebbi kód hívódik meg, hiszen az x típusa szerint csak ez a változat érhető el. A z.Beindit() esetében azonban a z típusa szerinti újabb metódus-változat hívódik meg.

Felüldefiniált metódus hívása más metódusból

Egy komoly gond van ezzel a 'new'-al felüldefiniált metódusok esetén: gondok vannak azokkal az esetekkel, amelyekben ezen metódusokat nem kívülről hívjuk meg, hanem "belülről", egy másik metódusból.

A kívülről való direkt meghívás esetén a fordítóprogram a példány típusa alapján ki tudja következtetni, melyik metódust is kívánjuk meghívni (lásd fenti példa). Ugyanakkor nézzük az alábbi esetet:

class TAuto
{
   public void Beindit()
   {
     if (motorMegy) throw new Exception("Nem illik jaro motorra rainditani!");
     motorMegy = true;
   }
   
   public void Leallit()  
   {
     motorMegy = false;
   }
   
   public void Hatramenet(int meter)
   {
     if (motorMegy) Console.WriteLine("Az autó hátramenetben gurul {0} métert",meter);
     else throw new Exception("Ha nem megy a motor, hogy akarsz hátragurulni?");
   }
   
   public void Beparkol()
   {
     Beindit();
     Hatramenet(2);
     Leallit();
   }
}

class TOkosAuto:TAuto
{
  new public void Beindit()
  {
    if (motorMegy) return;
    base.Beindit();
  }
}

class FoProgram
{
  public static void Main()
  {
     Tauto x = new TAuto();
     x.Beindit();
     x.Leparkol();
     
     TOkosAuto z = new TOkosAuto();
     z.Beindit();
     z.Leparkol();
  }
}
 

A fenti kódban az x példány esetén gondok lesznek, mivel az x.Beindit() esetén a motor elindul, az x.Beparkol() pedig első lépésben szintén meghívja a Beindit() metódust, mely jelen esetben a TAuto belsejében deklarált Beindit() lesz, amely rosszul reagál a járó motorral történő újbóli beindításra.

Az azonban sokkal érdekesebb, hogy a z példány esetén pontosan ugyanezt tapasztalhatjuk! Az történik, hogy a járó motor esetén ugyanarra a hibára fut a program!

Ennek a jelenségnek az oka a korai kötésben keresendő!

Hernyák Zoltán
2013-03-17 18:46:57