fel
le

Konstruktorok

A konstruktorok speciális feladatú metódusok. Feladatuk a frissen létrehozott objektum-példány alaphelyzetbe állítása.

Mint korábban említettük, az OOP világában az objektum-osztály egy típusnak tekintendő, melynek vannak adattároló részei (mezők), és műveletei a tárolt adatokon (metódusok).

A típusból létrehozott konkrét példányok felelősek saját maguk helyes működéséért. Ebbe beletartozik, hogy köteles védeni saját mezőik értékeit oly módon, hogy megakadályozzák az értelmetlen és értelmezhetetlen értékek befogadását, hogy később a műveletei végrehajthatóak legyenek. Ehhez hozzátartozik a kezdőértékek beállításának problémaköre is. A példánynak el kell tudni érnie azt, hogy létrehozása pillanatában máris eleve helyes és értelmes adatokkal legyen inicializálva, hogy létrehozása után is már helyesen működjenek a műveletei.

Ezt támogatandó, az OOP fogalmak közé tartozik a konstruktor mint speciális metódus. Ezen metódus speciális lefutásának időpontjában (a példány létrehozásakor, annak első használata előtt), és feladatában (a példány alaphelyzetbe állítása).

A programozónak a példányosításkor kötelező a konstruktort meghívnia. Megfelelően tervezett OOP nyelv ezt szintaktikai előírásokkal kötelezővé is teszi. Ilyen nyelv a Java és a C# is. A Delphi nyelvben azonban lehetőségünk van a példány deklarációja után, a konstruktor hívását megelőzően is használni a példányt, mely ilyenkor természetesen nem működhet megfelelően, és jó eséllyel futási hibára vezet ez a viselkedés. Jobb megközelítés azonban a Java és C# nyelv esete, amely nyelvi szintaktikai szinten teszi ezt kötelezővé. Ily módon a fenti nyelveken a konstuktor hívásáról a programozó megfeledkezhet, de ekkor a program szintaktikai hibás lesz, és a fordító le sem hajlandó azt fordítani.

Vegyük például a Random osztályból példányosítás esetét:

Random rnd = new Random();
 

A fenti kódban létrehoztunk egy példányt a Random osztályból, rnd névvel. A példányosításhoz használni kell a new kulcsszót, mely a memóriafoglalást végzi. Azonban, mint a fenti példában is látszik, a new-nak paraméterként meg kell adni az adott osztály konstruktorát is. A konstruktort jelen példában szintén Random-nak hívják (ez nem véletlen), és mivel itt metódushívásról van szó, ki kell tenni a függvény-hívó operátort is (gömbölyű zárójelpár).

Konstruktorok írása

Amennyiben valamely osztályhoz konstruktort szeretnénk készíteni, meg kell tanulnuk a készítésére vonatkozó szabályokat:

  • A konstruktorok általában public védelmi szintűek (de később majd látunk ezzel ellentétes példát is)
  • A konstruktoroknak nincs visszatérési típusuk (nem függvények), még a void kulcsszót sem szabad kiírni.
  • A konstruktorok mint metódusok neve meg kell egyezzen az osztály típus-nevével.
  • A konstruktoroknak lehet paraméterük, ez esetben a formális paraméterlistát természetesen fel kell tüntetni.

Mivel minden egyes konstruktor neve kötelezően megegyezik az adott osztály nevével, így a konstruktorok közötti különbséget a paraméterlistájukban kell keresnünk. Nem létezhet két azonos paraméterezésű konstruktor ugyanazon osztály belsejében, mivel ez esetben nem lehetne híváskor azonosítani, melyiket is szeretnénk meghívni.

Ha azonban paraméterezésükben különbözőek, máris alkalmazhatjuk a overloading szabályt, mely egyébként nemcsak konstruktorokra, hanem bármely metódusra is alkalmazható. Ezen szabály kimondja, hogy ugyanazon osztály tartalmazhat azonos nevű metódusokat, amennyiben azok különböznek formális paraméterlistájukban. Ez esetben ugyanis a hívás helyén feltüntetett aktuális paraméterlista alapján a fordító el tudja dönteni, az melyik formális paraméterlistára illeszkedik, és ez alapján ki tudja választani az azonos nevűek közül azt az egyet, amelyet meg kívánunk hívni.

Példaképpen álljon itt egy Téglalap objektumosztály, mely tárolja egy kétdimenziós környezetbeli téglalap bal felső sarkának X,Y koordinátáit, valamint a téglalap szélességét és magasságát:

class Teglalap
{
 public int X;
 public int Y;
 public int Szeles;
 public int Magas;
 
 // 1
 public Teglalap()     
 {
    X=0;
    Y=0;
    Szeles = 10;
    Magas = 10;
 }
 
// 2
 public Teglalap(int aOldal)
 {
     X = 0;
     Y = 0;
     Szeles = aOldal;
     Magas = aOldal;
 }

 // 3
 public Teglalap(int aSzeles, int aMagas)
 {
     X = 0;
     Y = 0;
     Szeles = aSzeles;
     Magas = aMagas;
 }

// 4
 public Teglalap(int aX, int aY, int aOldal)
 {
     X = aX;
     Y = aY;
     Szeles = aOldal;
     Magas = aOldal;
 }

 // 5
 public Teglalap(int aX, int aY, int aSzeles, int aMagas)
 {
     X = aX;
     Y = aY;
     Szeles = aSzeles;
     Magas = aMagas;
 }

}
 

A fenti példában 5 konstruktort írtunk ugyanahhoz az osztályhoz, más-más paraméterezéssel. A 'felhasználó' - az a programozó, aki az osztályunkat példányosítani szeretné - választhat melyik konstruktort használja:

// 10x10-es téglalap, bal felső sarka az origóban
Teglalap a = new Teglalap();    

// 20x20-es téglalap, bal felső sarka az origóban
Teglalap b = new Teglalap(20);  

// 20x30-es téglalap, bal felső sarka az origóban
Teglalap c = new Teglalap(20,30);      

// 40x40-es téglalap, bal felső a 30,15 pozíción lesz
Teglalap d = new Teglalap(30,15,40);    

// 40x60-es téglalap, bal felső a 30,15 pozíción lesz
Teglalap e = new Teglalap(30,15,40,60);
 

Mint látjuk, minden egyes példányosításnál valamelyik konstruktort ki kell választani. A fenti példában ugyan 5 db konstruktorunk van, mely közül bármelyik képes a példányt alaphelyzetbe állítani. Ezt az alaphelyzetet mindegyik másképp képzeli el, de mindegyik helyes alaphelyzet, utána a téglalappal máris dolgozni lehet.

Amennyiben valamely osztályhoz nem fejlesztünk ki olyan konstruktort, melynek üres a paraméterlistája, úgy a példányosításkor kötelező a paraméteresek közül választani:

class Kor
{
  public int sugar;
  public Kor(int aSugar)
  {
    sugar = aSugar;
  }
  public double Kerulet()
  {
     return 2*sugar*System.Math.PI;
  }
}
 

Példányosítása:

// szintaktikai hibás: nincs ilyen paraméterezésű konstruktor
Kor k = new Kor();

// helyes: az egyparaméteres kör konstruktor hívása példányosításkor
Kor f = new Kor(10);
Console.WriteLine("Kerulet={0}",f.Kerulet());
 

Alapértelmezett konstruktor

Mi történik akkor, ha kifejlesztünk egy olyan objektum-osztályt, melyhez nem készítünk konstruktort? A probléma az, hogy a C#-ban nyelvi szinten, szintaktikai szinten kötelező valamely konstruktort meghívni. Ha nem írunk konstruktort, akkor nem lehet példányosítani az adott osztályból?

class Haromszog
{
  public int oldalA;
  public int oldalB;
  public int oldalC;

  public double Kerulet()
  {
     return oldalA+oldalB+oldalC;
  }
}
 

Példányosítás:

Haromszog h = new Haromszog();
 

A fenti kód azonban mégis működik. Ennek oka, hogy amennyiben mi nem készítünk valamely osztályhoz konstruktort, akkor megteszi helyettünk azt a C# fordító. Elkészít egy olyan konstruktort, amelynek üres a paraméterlistája, és az utasításblokkja is. Ennek a konstruktortnak csakis az a szerepe, hogy a példányosításkor meg lehessen hívni. Azonban vegyük észre, hogy ezen konstruktor a mezőknek nem ad kezdőértéket, tehát alkalmazása komolyabb, professzionálisabb esetekben nem ajánlott.

class Haromszog
{
  .
  .
  .
  // ez explicit módon nincs benne a kódban, de a fordító 'belelátja', generálja
  public Haromszog()
  {
  }
}
 

A fenti, automatikusan generált konstruktort alapérelmezett konstruktornak nevezzük.

Ilyen konstruktort a fordító csakis akkor készít el helyettünk, ha mi egyáltalán nem írunk konstruktort. Amint azonban írunk legalább egy sajátot, a fordító úgy tekinti, hogy a példányosítás máris megoldható, tehát az ő áldásos tevékenységére nincs szükség. Ezért továbbra is igaz az az állítás, hogy a 'Kor' objektum-osztályból a new Kor() módon nem lehet példányosítani, hiszen ahhoz alapértelmezett konstruktor nem készült.

Saját másik konstruktor hívása konstruktorból

Ugyanahhoz az osztályhoz több, paraméterezésben különböző konstruktor is készülhet. Ekkor lehetőség van egyik konstruktorból valamely másik meghívására. A meghívásnak speciális szintaktikája van. A hívó konstruktor formális paraméterlistája mögé, a konstruktortörzs előtt kell egy kettőspont mögött feltüntetni. Amennyiben valamely másik saját osztálybeli konstruktort kell meghívni, akkor a használandó kulcsszó a this:

class Teglalap
{
 public int X;
 public int Y;
 public int Szeles;
 public int Magas;
 
 // 1
 public Teglalap()
   :this(0,0,10,10)
 { }
 
 // 2
 public Teglalap(int aOldal)
   :this(0,0,aOldal,aOldal)
 { }

 // 3
 public Teglalap(int aSzeles, int aMagas)
   :this(0,0,aSzeles, aMagas)
 { }

 // 4
 public Teglalap(int aX, int aY, int aOldal)
   :this(aX,aY,aOldal,aOldal)
 { }

 // 5
 public Teglalap(int aX, int aY, int aSzeles, int aMagas)
 {
     X = aX;
     Y = aY;
     Szeles = aSzeles;
     Magas = aMagas;
 }

}
 

A fenti példában is látható, hogy az 1..4 konstruktorok végrehajtása vissza van vezetve az 5-os konstruktor esetére, vagyis pusztán annyit teszünk, hogy a hiányzó paramétereket kiegészítjük, és meghívjuk az 5-os jelű konstruktort. Az a paraméterértékeket elhelyezi a mezőkben, így az 1..4 konstruktorok törzse e pillanattól kezdve akár üres is lehet, hiszen a mezőkbe máris bekerültek a kezdőértékek.

Nem publikus konstruktorok

Az előző esetben vetődhet fel először annak értelme, hogy az adott osztályhoz ne csak publikus, hanem akár private, akár protected konstruktort fejlesszünk ki. Ekkor készíthetünk olyan konstruktort, amelyet a külvilág a példányosításkor direktben nem tud meghívni, de a kiválasztott, publikus konstruktor az osztály belsejében már akár egy másik nem publikus (private, protected) konstruktort is meghívhat a this segítségével:

class A
{
    public A(int x, int y):this(true)
    {
      ...
    }

    private A(bool visible)
    {
      ...
    }
}
 
Hernyák Zoltán
2013-03-17 19:21:23