fel
le

Abstract osztályok

Az osztályok fejlesztése során kerülhetünk olyan szituációba, hogy valamely metódusunk törzsét nem tudjuk megírni, bár maga a metódus feladata már ismert. Ugyanakkor egyéb metódusokat meg tudunk írni, s ezek viszont ezen megírhatatlan metódusokat máris meghívnák, használnák.

Megoldási próbálkozás

class AltalanosGrafikusObjektum
{
   protected int x;
   protected int y;
   
   public void Kirajzol() {}
   public void Letorol() {}
   
   public void Athelyez(int eltolasX, int eltolasY)
   {
      Letorol();
      x += eltolasX;
      y += eltolasY;
      Kirajzol();
   }
}
 

A fenti példában a Kirajzol() illetve Letorol() eljárások törzsét nem tudjuk kidolgozni, mert az osztály annyira általános, hogy nem ismert számára milyen alakú a szóban forgó grafikus objektum. Ha már tudnánk hogy ez konkrétan kör, vagy négyzet, vagy pont, akkor meg tudnánk írni a két metódusunk törzsét.

Ugyanakkor az Athelyez(..) metódus ettől függetlenül elkészíthető, hiszen az ő törzse minden további nélkül megírható. Ebben hivatkozunk a két kidolgozatlan metódusra. Letöröljük a grafikus objektumot az aktuális helyéről, kiszámoljuk annak új koordináta-párját, majd meghívjuk a Kirajzol(...) metódust, hogy az új pozíción megjelenjen a grafikus objektumunk.

Az azonnal látszik, hogy a fenti megoldás nem jó. A korai kötés miatt a gyermekosztályokban hiába definiálnánk felül a két metódust, ezen újabb verziókat az örökölt Athelyez(...) nem használná:

class Kor:AltalanosGrafikusObjektum
{
   new public void Kirajzol()
   {
     ...
   }
   new public void Letorol()
   {
     ...
   }
}
 

Virtuális metódusokkal való megoldás

Az ilyen jellegű metódusok esetén a késői kötés gyakorlatilag kötelező, hiszen a gyermekosztály ezen metódusokat kötelező jellegűen ki kell dolgozza. Használjuk hát a virtual és override kulcssszavakat:

class AltalanosGrafikusObjektum
{
   protected int x;
   protected int y;
   
   public virtual void Kirajzol() {}
   public virtual void Letorol() {}
   
   public void Athelyez(int eltolasX, int eltolasY)
   {
      Letorol();
      x += eltolasX;
      y += eltolasY;
      Kirajzol();
   }
}

class Kor:AltalanosGrafikusObjektum
{
   public override void Kirajzol()
   {
     ...
   }
   public override void Letorol()
   {
     ...
   }
}
 

Ezzel a megoldással valójában csak egy a baj: a gyermekosztály fejlesztője nem köteles a virtuális metódusokat felüldefiniálni. Amennyiben "elfelejti" mindkét metódust kidolgozni, konretizálni, akkor fura viselkedést fog tapasztalni:

class Kor:AltalanosGrafikusObjektum
{
   public override void Kirajzol()
   {
     ...
   }
}
 

A fordítóprogram, sem senki más nem ad figyelmeztető jelzést a programozónak, hogy elfelejtkezett a Letorol(..) metódus megírásáról.

Másik probléma, hogy a fordítóprogram a fenti, virtual metódusokat teljes értékű metódusoknak tekinti, és lefordítja azok törzsét (mégha az amúgy üres is), és a lefordított .exe-kbe ezek helyet foglalnak (mégha nem is túl sokat).

Harmadik probléma, hogyha ezen metódusok nem void visszatérési típusúak, akkor az üres törzsű változatban is definiálni kell visszatérési értéket:

class AltalanosGrafikusObjektum
{
   // ez itt hibás: kötelező return-t írni a fv belsejébe
   public virtual int Kerulet() { }
}
 

Helyette:

class AltalanosGrafikusObjektum
{
   public virtual int Kerulet() { return 0; }
}
 

Miért adjunk vissza értéket ilyen függvényekben, amikor ezen függvényváltozatot sosem kellene meghívni, hiszen a cél az, hogy a gyermekosztály ezen függvényt mindenképpen definiálja felül override-al.

Utolsó probléma: a fenti osztály gyakorlatilag nincsen készen. Nem azért írtuk meg, hogy ezen osztályból példányokat készítsen bárki is, pusztán azért írtuk meg, hogy alapjául szolgáljon a további ilyen jellegű osztályok fejlesztéséhez. Ugyanakkor a fordítóprogram erről "nem tud", vagyis megengedi a példányosítást belőle.

Abstract osztályok

Az ilyen jellegű osztályok helyes módszerű készítése során egy speciális kulcsszót kell használni, az abstract kulcsszót. Ez minden fent ismertett problémánkat megoldja:

abstract class AltalanosGrafikusObjektum
{
   protected int x;
   protected int y;
   
   public abstract void Kirajzol();
   public abstract void Letorol();
   public abstract int Kerulet();
   
   public void Athelyez(int eltolasX, int eltolasY)
   {
      Letorol();
      x += eltolasX;
      y += eltolasY;
      Kirajzol();
   }
}
 

A metódusok írása során a nem kidolgozható metódusokat is megjelöltük az abstract kulcsszóval. Ezzel jelezzük, hogy sajnos nem tudjuk megírni őket, de deklaráljuk őket. Megadjuk nevüket, és paraméterezésüket. Törzs helyett ez esetben nem üres törzset jelölünk ({}), hanem egyáltalán nem írunk hozzájuk utasításblokkot, helyette pontosvesszőt teszünk.

Ez vonatkozik a függvény jellegű abstract metódusokra is. Így megússzuk a return írását ezen függvényekbe.

Az ilyen metódusokat más metódusokból (pl. Athelyez(...)) meg szabad hívni, hiszen nevük és paraméterezésük ismert, a fordítóprogram le tudja ellenőrízni hogy a hívás helyén megfelelő számú és típusú aktuális paramétert adunk-e át.

Az ilyen metódusok hívásának kötése persze nem korai kötésű, hiszen ezen metóduspéldányok lényegében nem léteznek, nincsenek megírva. Helyette majd a késői kötés lesz használva, hogy a gyermekosztályokbeli felüldefiniált változatok kerüljenek meghívásra.

Ha egy osztály tartalmaz abstract metódusokat, akkor magát az osztályt is meg kell jelölni ezzel a kulcsszóval: abstract class AltalanosGrafikusObjektum. Az absztrakt osztályokból nem lehetséges a példányosítás, mivel a fordítóprogram ezt megtiltja.

Abstract osztályok továbbfejlesztése

Az absztrakt osztályokat nem csak hogy lehet, de lényegében kötelező továbbfejleszteni, ugyanis magából az absztrakt osztályból nem lehet példányosítani.

A továbbfejlesztés során az absztrakt osztályt a szokásos módon ősnek deklaráljuk. Az örökölt absztrakt metódusok késői kötésűek, ezért felüldefiniálásukhoz a szokásos override kulcsszót kell használni:

class Kor:AltalanosGrafikusObjektum
{
   public override void Kirajzol()
   {
     ...
   }
}
 

Amennyiben elfelejtkeznénk valamely örökölt absztrakt metódus felüldefiniálásáról (mint jelen példában a Letorol() metódusról, úgy a fordítóprogram erre figyelmeztet. Hogyan? Egyszerű! A fenti esetben a Kor osztály is tartalmaz absztrakt metódust, hiszen az örökölt, és nem felüldefiniált Letorol() még absztrakt volt. E miatt maga a Kor osztálynak is absztraktnak kell lennie! Mivel ezt azonban nem írtuk oda az osztály neve mellé, így a fordítóprogram ezt hibának tekinti.

A hiba észlelése után nyilván a programozó utánanéz, miért is tekinti a fordítóprogram hibásnak az osztályát, és észreveszi a hiányzó, kidolgozatlan metódust. Ezek után két dolgot tehet:

  • ha a metódust amúgy meg tudja írni, csak "elfelejtkezett" róla, akkor megírja, és a fordító nem követeli tovább az abstract class jelzés kirakását az osztályhoz.
  • ha a metódust a programozó még ezen a szinten sem tudja kidolgozni, akkor értelemszerűen nem írja meg, de kiteszi a fordító által megkövetelt abstract class jelzést. Ekkor ezen gyermekosztályból sem lehet a későbbiek során majd példányosítani. A metódus kidolgozását újra elhalasztottuk.

Abstract property

Nem csak metódusok lehetnek absztraktok, hanem property-k is. Ezekre gyakorlatilag ugyanazok a szabályok vonatkoznak, mint a metódusok esetén tárgyaltakra:

  • ha egy osztály absztakt metódust tartalmaz, akkor nem lehet belőle példányosítani, és ki kell tenni az abstract class jelzést
  • a gyermekosztály a property-t kidolgozhatja, override segítségével, és akkor nem kell kitennie az absztrakt jelzést magára
  • a gyermekosztály sem tudja kidolgozni a szóban forgó property-t, ekkor ki kell tennie az absztrakt jelzést magára.

abstract class Valami
{
    abstract public int x { get; }
}

class Sajat:Valami
{
    protected int _x;
    override public int x
    {
       get
       {
          return _x;
       }
    }
}
 

Amennyiben az ős osztály csak a get meglétét tette virtuálissá, úgy a gyerekosztály a set részt már sajnos nem dolgozhatja ki semmilyen formában.

Hernyák Zoltán
2013-03-17 18:47:24