Személyes eszközök
Keresés

 

A InfoWiki wikiből



Tartalomjegyzék

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 konstruktortjelen 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 (gombölyű zárójelpár).

Konstruktorok írása

Amennyiben valamely osztályohoz 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, aSzeles) {}
 
         // 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)
    {
      ...
    }
}


Konstruktor hívási lánc

Tegyük fel, hogy a példányosítandó osztályunk neve C, őse valamely B, akinek az őse valamely A osztály, akinek már nincsen őse. A példányosításkor meg kell hívnunk a C osztály valamely konstruktorát, amely lefut, és beállítja a példányt alaphelyzetbe. Említettük, hogy ehhez a C osztály bármely konstruktora önállóan is elég kell legyen, mi példányosításkor csak egy konstruktort kívánunk meghívni. A többi a kiválasztott konstruktor dolga, oldja meg (ha kell a this segítségével hívja segítségül valamely másik konstruktort is).


class A
 {
     private int h;
     protected double g;
     public A() {  ... }
     public A(int x) {  ... }
 }
 
class B:A
 {
    protected string s;
    public B() { ... }
    public B(int a) { ... }
 }
 
class C:B
 {
    public bool l;
    public C(int f, int d) { ... }
    public C(int f):this(f,10.0) { ... }
 }


Mi a helyzet azonban az ős osztályok konstruktoraival? Kell-e hogy a példányosításkor az ős osztályok

konstruktora is lefusson?

Gondoljunk bele, hogy a gyermekosztálya a feladat megoldásához nem lehet elég minden esetben! Az ős osztálynak lehetnek private mezői (pl. az A osztálynak van ilyen mezője), melynek kezdőértékét a gyermekosztály konstruktora nem képes beállítani, révén hogy nem is látja (nincs is tudomása) a mező létezéséről. Ugyanakkor ez a tény önnmagában nem mentesít bennünket a private mező kezdőértékének beállítása alól.

A megoldás a következő: az ős osztályok konstruktorai közül is legalább egynek le kell futnia a példányosításkor. Az ős osztályaink mindegyikéből (mindegyik szinten) egy konstruktornak. Legelőször a fejlesztési lánc legfelső szintjén lévő osztályból, majd annak a gyerekosztályából, egyre haladva lefelé a szinteken, legutoljára azon osztály konstruktorának, amelyből a konkrét példány készül.

Miért ebben a sorrendben? A legősebb ős konstruktora fut le legelőször. Ő beállítja a saját mezőinek a kezdőértékét, beleértve a private, protected, public mezőinek a kezdőértékeit. Aztán jön az ő közvetlen gyermekosztályában definiált valamely konstruktor, aki az előző konstruktor által beállított kezdőértékeket felüldefiniálhatja, amennyiben szándékában áll (a protected és public mezőkét direkt módon, a private mezők kezdőértékeit megfelelő örökölt metódushívásokon keresztül). Majd jön a következő szint konstruktora, egészen a legalsó, legfeljletteb gyermekosztály konstruktoráig. Neki még lehetősége van minden előző szinten lévő konstruktor beállításait felülbírálni, befejezni a mezők beállításait. Ezen konstruktor amikor végez - akkor a példány készen van.

!!! KÉP !!!!


Konstruktor hívási lánc működése

C pld = new C(30);

A fenti példányosítás során a C osztályból kiválasztjuk a két konstruktor közül az egyparaméterest. Mielőtt ezen egyparaméteres lefutna, a 'this(f,10.0)' miatt lefut a C másik konstruktora, a kétparaméteres. De mielőtt az lefutna, le kell fusson az ős osztály (B) valamely konstruktora. De melyik? A B osztályban két konstruktor is van!

Mivel nincs explicit módon jelezve, hogy melyik konstruktor fusson le a B osztályból, ilyenkor a fordító választja ki azt automatikusan. Ő viszont nem képes választani, csak a paraméter nélküli változatot, mivel a másik, a paraméteres B konstruktor meghívásához kellene ugye paraméter-értéket annak átadni. Node a fordító nem állít elő paraméter-értékeket a 'semmiből', még akkor sem, ha azok ilyen egyszerű típussal rendelkeznek, mint jelen esetben az 'int'. Ezért ha a fordítónak kell választania, akkor ő a paraméter nélkülit fogja választani.

Ugyanakkor mielőtt a B paraméter nélküli konstruktora lefutna, előtte le kell fusson az ősének, az A osztálynak is valamely konstruktora. Jelen példában szintén a fordítónak kell választania, ő megint csak a paraméter nélkülit tudja, és fogja kiválasztani. Tehát a konstruktorok lefutási sorrendje:

1. class A -> A()
2. class B -> B()
3. class C -> C(30,10.0)
4. class C -> C(30)

Konstruktor hívási lánc problémái

Gond van akkor, ha a fordító nem tud választani paraméter nélküli konstruktort a felsőbb szintről automatikusan:

class alfa
 {
    protected string s;
    public alfa(int a) { ... }
    public alfa(int a, int b) { ... }
 }
 
 class beta:alfa
 {
    public bool l;
    public beta(int f, int d) { ... }
    public beta(int f):this(f,10.0) { ... }
 }
 
 ...
 beta x = new beta(12,34.3);


Fenti esetben a beta osztályból példányosítanánk, a kétparaméteres konstruktor segítségével. Ugyanakkor mielőtt az lefutna, le kell fusson az ős osztályának valamely konstruktora. De melyik? A fordító nem tudja automatikusan aktiválnijelen példában egyik konstruktort sem az alfa osztályból, mivel mindegyik vár legalább egy paramétert.

Ezen probléma a fenti példában nem feloldható. Ha ilyen kódot írunk, akkor a fordító már a beta osztály lefordításakor szintaktikai hibát jelez, figyelmeztetve hogy nem képes feloladni a konstruktor kiválasztási folyamatot, így az alfa osztályból a létező konstruktorai ellenére sem lehet példányosítani.

Ős osztály konstruktorának hívása konstruktorból

A fenti problémára megoldást kell keresni, és nyilván van is. A fentihez hasonló esetben, amikor a fordító nem tud választani saját hatáskörében az ős osztály konstruktorai közül (vagyis gyak. nem létezik az ős osztályban paraméter nélküli konstruktor), akkor nekünk kell 'kézzel' a kiválasztást elvégezni, jelezni kell melyik paraméterezésű ős konstruktort kell kiválasztani, milyen aktuális paraméter-értékekkel, azt a program kódjában explicit módon deklarálni kell. Erre a base kulcsszó használandó:

class alfa
 {
    protected string s;
    public alfa(int a) { ... }
    public alfa(int a, int b) { ... }
 }
 
 class beta:alfa
 {
    public bool l;
    public beta(int f, int d):base(f) { ... }
    public beta(int f):this(f,10.0) { ... }
 }
 
 ...
 beta x = new beta(12,34.3);

A fenti példában a base(f) azt jelzi, hogy az ős osztályból annak az egyparaméteres konstruktorát kell aktiválni, a paraméter értékét is specifikáltuk. Így az alul jelzett példányosítás esetén az alábbi sorrendben hajtódnak végre a konstruktorok:

1. class alfa -> alfa(12)
2. class beta -> beta(12, 34.3)

Mikor kötelező a base használata?

Amikor az ős osztálynak egyáltalán nincs paraméter nélküli konstruktora, akkor a fordító nem tud önállóan választani konstruktort, így kötelező a base(...) segítségével nekünk választani (lásd fenti példa).

Szintén kötelező a base használata, amikor az ős osztálynak van paraméter nélküli konstruktora, de az 'private' védelmi szintű. A private konstruktort a gyermekosztály elvileg nem láthatja, tehát nem is hívhatja meg, még automatikusan sem. Ilyenkor szintén a gyermekosztályban is elérhető, protected vagy public konstruktorokból kell választani explicit módon, a base használatának segítségével:


class gerinces
 {
    protected string neve;
    public gerinces(int a):this() { ... }
    private gerinces() { ... }
 }
 
 class emlos:gerinces
 {
    public emlos():base(100) { ... }
    public emlos(int f):base(f) { ... }
 }
 
 ...
 emlos kacsa = new emlos(35);

A fenti esetben az emlos osztályban azért kell a base segítségével az ős osztályból explicit módon kiválasztani a paraméteres konstruktort, mert a paraméter nélküli nem elérhető a gyerekosztályban, ezért a fordító sem fogja azt automatikusan kiválasztani.

Konstruktor hívási lánc működése (mégegyszer)

Vegyük észre, hogy a példányosítás során a gyerekosztályból egyértelműen kiválasztunk egy konstruktort (a paraméterezés alapján). Ezen kiválasztott konstruktor :

  • this(...) segítségével meghívhat egy másik saját konstruktort, ekkor az ős osztályból a konstruktorválasztás problémája elodázásra került, ezen másik saját konstruktort kell megvizsgálni a továbbiakban.
  • base(...) segítségével explicit módon választhat ki egyet az ős osztály konstruktorai közül
  • sem this(...) sem base(...) nem szerepel. Ekkor az ős osztály konstruktorai közül implicit módon a paraméter nélküli kerül kiválasztásra.
  • Ha az ős osztályból aktiválandó konstruktor sem explicit, sem implicit módon nem került kiválasztásra, akkor hiba van, a példányosítás nem kivitelezhető. Szerencsére ezt a fordítóprogram már fordítási időben ki tudja szűrni, így fordítási, szintaktikai hibával leáll.

Amennyiben akár explicit, akár implicit módon az ős osztály konstruktora kiválasztásra került, a problémra máris áttevődik erre a szintre: melyik konstruktort kell meghívni az ős-ősének szintjértől!? A kérdés megválaszolására újra végig kell járni a fenti négy pontot. Vagy az ottani this(...) miatt a válasz elodázásra kerül, vagy van explicit kiválasztás (base(...)), vagy van ennek hiányában implicit paraméter nélküli kiválasztás. Egyéb lehetőségek nincsenek.

Ezért a legalsó, példányosításra kerülő szinten a példányosítás során használt konstruktor egyértelmű kiválasztása mintegy láncreakció-szerűen minden felsőbb szintről kiválasztja az onnan használandó konstruktorokat. Hogy ez kivitelezhető-e, ezt fordításkor el lehet dönteni, és fordító meg is vizsgálja, hogy a legalsó szint bármely konstruktorának kiválasztása esetén ezen hivási lánc egyértelműen felépíthető-e. Ha nem, akkor már fordításkor hibát jelez.

Példányosítás megakadályozása private konstruktorral

Amennyiben el szeretnénk érni, hogy egy osztályból a külvilág ne tudjon példányt készíteni (ilyen osztály pl. a Console osztály), úgy ezt beláthatjuk, hogy azzal semmit sem érünk el, ha a Console osztályba egyáltalán nem készítünk konstruktort. Ugyanis ekkor a fordító automatikusan pótolja a 'hiányosságot', és elkészíti az alapértelmezett konstruktort, amelyen keresztül minden további nélkül lehet példányosítani.

Az már közelebb visz a megoldáshoz, ha készítünk valamilyen konstruktort a szóban forgó osztályba, de az ne legyen 'public', hiszen akkor őt a kód bármely pontjáról meg lehet hívni, és lehet példányosítani. A 'protected' konstruktort már sokkal jobb:

class Vedettosztaly
{
    protected Vedettosztaly() 
    {
       ... 
    }
}  
...
Vedettosztaly x = new Vedettosztaly();

A fenti kódba szereplő példányosítás nyilván nem működik, hiszen a 'protected' védelmi szint nem teszi lehetővé az osztályon kívüli elérhetőséget, tehát a példányosítás pontján ez a 'metódus' nem meghívható. De ez a fajta védelem könnyen megkerülhető:

class Hacked:Vedettosztaly
{
}  
...
Hacked x = new Hacked();

A fenti 'Hacked' osztály a 'Vedettosztaly' leszármazottja, de megírásába nem fektettünk túl sok energiát. Konstruktort sem írtunk bele, mivel még csak arra sincs szükség. A fordító automatikusan bele fogja rakni az alapértelmezett konstruktort, és az ős osztályból ki tudja választani a protected konstruktort, mivel egyrészt az paraméter nélküli, másrészt a protected metódusokat a gyerekosztály belseje el tudja érni. Így nincs probléma a konstruktort hívási lánccal, a példányosítás a 'Hacked'

osztályból kivitelezhető.

Ezen az sem segít, ha a védett osztályba paraméteres protected konstruktort helyezünk el. Ekkor kicsit több munka van a 'Hacked' megírásával, mivel tenni kell bele egy konstruktort, amely a fenti protected konstruktort a base segítségével, valami kamu paraméterekkel meghívja, és megintcsak készen vagyunk.

Teljesen megváltozik azonban a helyzet akkor, ha a védett osztály private konstruktor tartalmaz, sem protected sem publicot nem! Ekkor hiába próbálkoznánk a fenti trükkel, azt sem implicit, sem explicit módon nem tudjuk a gyerekosztályból meghívni. Ekkor a konstruktor hívási lánc nem felépíthető, így a példányosítás nem megvalósítható! Ezt a fordító egyébként már a 'Hacked' osztály fordításakor kiírná:

class Console
{
  private Console() { }
}

Object-factory

Amennyiben valamely osztály nem biztosít a külvilág részére publikus konstruktort a példányosításhoz, akkor is van elvi lehetőség a példányosításra. Ekkor azonban egy osztály szintű publikus példányosító metódust kell készítenünk. Az ilyen jellegű, feladatú metódusokat object factory-nak, példány-gyár-nak nevezzük:

class Vedettosztaly
{
 private Vedettosztaly() 
 { 
 }
 
 public static Vedettosztaly Letrehoz()
 {
   Vedettosztaly x = new Vedettosztaly();
   return x;
 }
}  
...
Vedettosztaly p = Vedettosztaly.Letrehoz();

A fenti példában szereplő osztály egyetlen létező konstruktora private, ezért osztályon kívülről nem példányosítható. Azonban a Letrehoz() osztályszintű metódus képes ezen private konstruktort elérni, így neki van lehetősége példányosítani saját magából, a létrehozott példányt pedig (pontosabban referenciáját, a memóriacímét) mint függvény visszatérési érték visszaadni a hívás helyére.

Nyilván felmerülhet a kérdés, hogy miért is van szükség arra, hogy egy osztály elrejtse a külvilág elől a konstruktorát, majd publikus lehetőséget adjon annak meghívására?! Nos, előfordulhat például az az eset, hogy az adott osztályból a példányosítás nem történhet meg minden körülmények között. Például köthetjük újabb példányok létrehozásának lehetőségét megfelelő szabad memóriakapacitáshoz, vagy más erőforrás rendelkezésre állásához. Esetleg a program demó verziójában csak néhány (maximált) példány létrehozására van lehetőség, stb. Hogyan lehet ezt megvalósítani?

class Vedettosztaly
{
  private Vedettosztaly() {  }
 
    private static int szamlalo = 0;
    public static Vedettosztaly Letrehoz()
    {
       if (szamlalo<10)
       {
          szamlalo++;
          Vedettosztaly x = new Vedettosztaly();
          return x;
       }
       return null;
    }
}  
...
Vedettosztaly p = Vedettosztaly.Letrehoz();

A fenti példában egy adott programlefutás során a védett osztályból csak maximum 10 darab példányt lehet készíteni. A szamlalo mező is statikus mező, hogy az osztály számára egy közös ilyen számláló legyen a memóriában. A limit elérése után az object-factory már nem hoz létre újabb példányokat, hanem null értéket ad vissza.

Hasonló problémakör, amikor az osztályból készült példányokat egy listába kívánjuk automatikusan gyújteni:

class Vedettosztaly
{
    private Vedettosztaly() {  }
    public void valami()    { ... }
 
    public static ArrayList lista = new ArrayList();
    public static Vedettosztaly Letrehoz()
    {
       Vedettosztaly x = new Vedettosztaly();
       lista.Add( x );
       return x;
    }
}  
...
Vedettosztaly p = Vedettosztaly.Letrehoz();
foreach(Vedettosztaly v in Vedettosztaly.lista)
  v.valami();

A konstruktorok hibajelzése

Természetesen lehetőséget kell biztosítani arra is, hogy a konstruktor maga is megtagadhassa a példány létrehozását abban az esetben, amikor a paraméterértékek ismeretében a példány létrehozása értelmetlen.

Ilyen például a FileStream osztály konstruktora, amelynél paraméterként meg lehet adni egy file nevét, és annak megnyitási módját (írás, olvasás, stb). Amennyiben olvasási módban kívánjuk a file-t megnyitni, annak értelemszerűen léteznie kell. Ha ez nem teljesül, akkor a FileStream példány (amelynek műveletein keresztül ezen file-ban lévő adatokat ki kellene tudnunk olvasni) nem létrehozható!

Nagyon sok hasonló jellegű konstruktor említhető meg: hálózati kapcsolatot kiépítő példány (paraméter a célszámítógép IP címe vagy DNS neve - itt elképzelhető hogy a célszámítógép abban a pillanatban nincs is bekapcsolva), Xml file beolvasó példány (paraméter az XML file neve - itt elképzelhető, hogy az XML file hibás struktúrájú), stb.

A konstruktor mint metódus azonban nem adhat vissza a hívás helyére hibakódot, mivel egyáltalán nincs visszatérési értéke. Nem adhat vissza 'null' értéket, mivel nem adhat vissza semmit, és a memóriafoglalást amúgy is addigra a 'new' már elrendezte. Képernyőre hiába ír üzenetet, mert azzal a felhasználó nem fog tudni mit kezdeni. Semmilyen szokványos, egyszerű hibajelzési technika nem működik. A megoldást a kivételkezelésben kell keresni. Erről a könyv későbbi fejezetiben lesz szó, egyelőre annyit jegyeznénk meg, hogy ez egy szabványos kódrészek egymás közötti hibajelzési technikája. A 'throw' kulcsszó segítségével lehet a hibát jelezni. Maga a hiba is egy, a hiba körülményeit leíró objektumosztály egy példánya. Legegyszerűbb esetben ilyen osztály az 'Exception' osztály.

class FileStream
{
    public FileStream(string fileNev) 
    {  
      if (!File.Exists(filenev)) throw new Exception("A file nem létezik.");
      ...
    }
}

Ennek segítségével a fenti, korábban object-factory segítségével megoldott problémákat is kezelhetjük a konstruktorok belsejéből:

class Vedettosztaly
{
    private static int szamlalo = 0;
    public Vedettosztaly() 
    {  
       if (szamlalo<10) szamlalo++;
       else throw new Exception("Túl sok példányt akarsz létrehozni");
    }
}

A példányok felrakása listára problémaköre is kezelhető konstruktorból. A konstruktor (és egyéb példányszintű metódusok) belsejében az aktuális példányra a 'this' kulcsszóval lehet hivatkozni, erről is lesz még szó egy későbbi fejezetben:

class Vedettosztaly
{
    public static ArrayList lista = new ArrayList();
    public Vedettosztaly() 
    {  
       lista.Add( this );
       ...
    }
}

Tehát meg jegyeznénk, hogy amit az object-factory-k segítségével meg tudunk oldani, azok általában megoldhatóak a konstruktorokban is. De az object-factory-k sok problémát központosítva tudnak kezelni. A fenti számlálós esetben könnyű elképzelni, hogy amennyiben az osztálynak több konstruktora is lenne, akkor nem szabad kihagyni limit-elérése ellenőrzést egyikből sem. Ha mégis megtennénk, akkor kárbaveszne minden fáradozásunk, hiszen lehetne több mint 10 példányt készíteni. Az Object-Factory használata esetén egy helyen szerepel az ellenőrzés, így kisebb a hibalehetőség.

A példány szintű konstruktorok és a VMT

Szó volt arról, hogy minden egyes példányhoz hozzá kell rendelni a hozzá tartozó VMT táblát. A példány helyfoglalásába minden egyes esetben beleszámít a megfelelő VMT táblára mutató pointer (referencia) helyigénye is (+4 byte). Ezen VMT pointer feltöltése értelmes memóriacímmel értelemszerűen futás közben történik meg, a példány helyfoglalása után azonnal. Ez az adat konkrétan a konstruktor hívásakor történik meg automatikusan a futtató rendszer által. Amikor példányosítunk, akkor a new után kiválasztott konstruktor határozza meg, hogy melyik osztály VMT táblájának a memóriacíme rendelődjön hozzá a példányhoz.

Ezen hozzárendelés a C# és Java esetén már a konstruktor törzsének lefutása előtt megtörténik. Ezért a konstruktor belsejében már rendelkezésre áll a VMT tábla, és működik a késői kötés. Ez másképpen azt jelenti, hogy a konstruktor törzsében szabad hívni virtuális metódusokat.

Más nyelveken, mint a C++ is, ez nem így van. Ott ez a hozzárendelés csak a konstruktorok lefutása 'után' következik be, vagyis egy C++ konstruktor belsejében még nem működik a késői kötés.

Mivel ez implementációfüggő, ezért adott programozási nyelvet választva ezt a nyelvi specifikációból kell kideríteni. De egy egyszerű teszt programmal egyébként gyorsan felderíthető a dokumentáció olvasása nélkül is:

class Proba_A
{
    public Proba_A() 
    {  
       Kiiras();
    }
 
    public virtual void Kiiras() 
    {
      Console.WriteLine("Nem mukodik a kesoi kotes.");
    }
    }
 
    class Proba_B:Proba_A
    {
    public override void Kiiras() 
    {
      Console.WriteLine("Mukodik a kesoi kotes!");
    }
    }    
}

Amennyiben példányosítunk a Proba_B osztályból, akkor a konstruktor hívási lánc működésének megfelelően a 'Proba_A' osztály konstruktora is le fog futni. Ha addigra a VMT hozzárendelés már megtörtént, akkor működik a késői kötés, és a Proba_A konstruktor belsejében lévő 'Kiiras()' függvényhívás már az új verzió, a Proba_B osztálybeli függvény lesz, vagyis a 'Mukodik' kiírás fog megjelenni a képernyőn. Ellenkező esetben még nem működik a késői kötés mechanizmusa, és a korai kötés szabályai miatt a Proba_A-ban definiált Kiiras() metódus fog végrehajtódni.

A konstruktorok és a mezők szabályai

Amikor valamely mezőnk lehetséges értékére szabályt alkotunk (pl. nem lehet negatív szám értékű), akkor ezt a szabályt az osztályunknak érvényesítetteni és betartattni kötelessége.

Ennek érdekében a mezőt jellemzően nem publikusra vesszük fel, hiszen ekkor a külvilág a mi kontrollunk nélkül olvashatná és írhatná az értékét. Márpedig a külvilágtól sosem várhatjuk el, hogy betartja a "mi" szabályunkat.

A mezőt ekkor vagy private vagy protected védelmi szinttel látjuk el, és készítünk egy publikus property-t, melyen keresztül a külvilág a mezőnket írhatja és olvashatja. A property 'set' részében pedig a szabályt lekódoljuk, "betartattjuk".

Ez azonban még csak fél siker, hiszen a fenti megoldással a mezőbe később nem kerülhet hibás érték. De mi van a példány létrehozása utáni kezdőértékkel?

class Kor
{
    protected int _sugar;
    public int sugar
    {
      get
      {
         return _sugar;
      }
      set
      {
        if (value<=0) throw new Exception("Hibas ertek!");
        else _sugar = value;
      }
    }
}

A fenti kód biztosítani látszik, hogy a 'Kor' osztály példányaiban sosem lesz a sugár nulla, vagy negatív. Ez azonban nem teljesen igaz:

Kor a = new Kor();
Console.WriteLine("A kor sugara=",a.sugar);

A fenti kis program azt írja ki: 'A kor sugara=0'. Miről feledkeztünk el?

Arról, hogy a '_sugar' mező kezdőértéke '0'! Nincs rákényszerítve a külvilág, hogy a mező értékét példányosításkor beállítsa, így az a kezdőértékkel, a 0-val indul!

class Kor
{
    protected int _sugar;
    public int sugar  { get { ... } set { ... } }
    public Kor(int aSugar)
    {
      if (aSugar<=0) throw new Exception("Hibas ertek!");
      _sugar = aSugar;
    }
}
// ... főprogram ...
Kor a = new Kor(10);
Console.WriteLine("A kor sugara=",a.sugar);

Mivel írtunk a 'Kor' osztályhoz konstruktort, azt kötelező használni, így kötelező megadni a '_sugar' mező kezdőértékét. A konstruktor betartatja azt a szabályt a külvilággal, hogy a mező kezdőértékét kötelező megadni, a property betartatja azt a szabályt, hogy az érték nem lehet 0 vagy negatív. A két technika együtt garantálja a mezőre vonatkozó szabály teljes értékű betartatását!

Megjegyeznénk, hogy nem szerencsés a szabály (új érték <= 0) két helyen történő lekódolása (a konstruktorban és a propertyben is). A redundancia mindíg káros! Amennyiben valami későbbi módosítás miatt a szabály megváltozna (pl. megengedhetnénk a 0 értéket is), akkor két helyen kell módosítani a kódot.

Ennél ravaszabb, elegánsabb megoldás, ha a konstruktor belsejében a szabály feletti ellenőrzést átengedjük a propertynek:

class Kor
{
    protected int _sugar;
    public int sugar  { get { ... } set { ... } }
    public Kor(int aSugar)
    {
      sugar = aSugar;
    }
}

A fenti esetben a konstruktor az aktuális értéket 'aSugar' nem direktben a fizikailag is létező mezőbe tárolja el, hanem a property-be írja azt. Ekkor aktiválódni fog a property 'set' része, és lefut az ellenőrzés. Ha az érték nem felel meg a szabálynak, akkor a property 'throw new Exception(...)'-el jelzi a hibát. Ennek működési mechanizmusa miatt ez olyan, mintha maga a konsruktort jelezte volna a hibát, így a példányosítás nem fog megtörténni.

Osztály-szintű konstruktorok

Az osztályszintű metódusok szerepe hasonló az eddig ismertetett példányszintű konstruktortok szerepével. A különbség annyiban van, hogy az osztályszintű konstruktorban az osztályszintű mezők kezdőértékeit tudjuk beállítani.

Mivel az osztályszintű (static) mezők kezdőértékét leggyakrabban a mező mellett kezdőértékadás formájában szoktuk megadni,ezért osztályszintű konstruktor írására ritkán kerül sor. De mindíg előfordulhat, hogy a mező kezdőértékét nem tudjuk megadni kezdőértékadással, mert a kifejezés túl bonyolult lenne. A kezdőértékadást definiáló kifejezés ugyanis csak egyszerű kifejezés lehet, tehát metódushívást, egyéb példányszintű mezőt mint operandust nem tartalmazhat. Különösen nem egyszerű a helyzet, ha a kezdőértéket valamely, akár ciklust és elágazást is tartalmazó algoritmus alapján kell kiszámolni.

Amint a kifejezés bonyolultsága nem teszi lehetővé a kezdőértékadáson keresztül történő beállítást, ezen értékadást át kell helyeznünk az osztályszintű konstruktor belsejébe.

Osztályszintű konstruktor írásakor tudnunk kell, hogy ezen konstruktor neve is megegyezik az osztály nevével csakúgy, mint a példányszintű esetben. A különbség, hogy ezen esetben a 'static' kulcsszót is meg kell adni, jelezvén hogy a konstruktor osztályszintű.

Az osztályszintű konstruktort a futtató rendszer fogja automatikusan (implicit módon) meghívni a megfelelő időben. Ez azzal a következménnyel jár, hogy az osztályszintű konstruktornak 'nem lehet paramétere', hiszen a futtató rendszer nem fog a semmiből paraméterértékeket kreálni a mi kedvünkért.

Ha a neve kötött, és nem lehet paramétere, akkor az 'overloading' szabály sem segíthet rajtunk: osztályszintű konstruktorból csakis egy darab létezhet minden egyes osztályban! Ezen maximum egy létező konstruktorral nem lehet már tovább trükközni: nem tehetjük a hecc kedvéért private sem protected módosítójúvá, ugyanis ez esetben a futtató rendszer nem tudná meghívni - akkor meg mi értelme van? Ezért ezen konstruktor kötelezően public. Ezt az akaratát a rendszer oly módon kényszeríti ránk, hogy nem írhatunk ki semmilyen védelmi-szint módosítót az osztályszintű konstruktorunk mellé.

Osztályszintű konstruktorból másik saját osztályszintű konstruktor hívása (':this()' módon) nyilván lehetetlen, mivel nem lehet másik osztályszintű konstruktor, ilyenből osztályonként csak egy lehet.

Az osztályszintű konstruktorok között a hagyományos értelembeli konstruktor-hívási lánc nem működik. Ugyanis az adott osztály osztályszintű konstruktorának meghívásának az időpontja nem ilyen egyértelmű szabályok szerint zajlik. A futtató rendszer igyekszik 'spórolni' a program futási idejével. Nem úgy működik a rendszer, hogy a program indulásának elején a programban használt osztályszintű konstruktorok mindegyike lefut, majd elindul a program érdembeli futása is a Main függvény végrehajtásával, hanem mindössze azt a szabályt igyekszik a futtató rendszer betartani, hogy az adott osztály static konstruktora garantáltan le kell fusson 'mielőtt' az adott osztállyal bárminemű műveletet is végezhetnénk (legyen az osztályszintű vagy példányszintű művelet). Vagyis az előtt garantált hogy lefut a konstruktor, hogy meghívhatnánk valamely osztályszintű metódust, hivatkoznánk valamely osztályszintű mezőre, esetleg példányosítanánk az osztályból, stb.

Az osztályszintű konstruktorok hívása tehát a program futása során 'elszórtan' történik meg, mindíg a lehető legkésőbbi, de még nem túl kései időpontban!

Ez azt is jelenti, hogy osztályszintű konstruktorból ős osztály osztályszintű konstruktor explicit hívása (':base()' módon) szintén értelmetlen, mivel az ős osztály ilyen konstruktora egyáltalán nem biztos, hogy korábban lefut, mint a gyerekosztály static konstruktora. Amennyiben a gyerekosztállyal korábban kezdünk el dolgozni, mint az ős osztállyal, úgy a gyerekosztályban definiált osztályszintű konstruktor korábban fog lefutni, mint az ősében definiált.

class Proba_A
{
    public const string strElvalaszto = ",;|";
    public static char[] arrElvalaszto;
    static Proba_A()
    {
        arrElvalaszto = strElvalaszto.ToCharArray();
    }
}

A fenti példában az 'arrElvalaszto' nevű osztályszintű mező kezdőértékét kívánjuk beállítani. Ez egy karaktereket tároló vektor kellene legyen, melyben az adatforrás felől érkező adatok elválasztó karaktereit kívánjuk felsorolni. De a .ToCharArray() példányszintű metódushívás, mely nem szerepelhet osztályszintű mezők kezdőértékadásában. Ezért ezt már kénytelenek vagyunk a static konstruktorban elhelyezni. Garantált, hogy a konstruktor lefut, mielőtt a program hivatkozhatna ezen mezőre, így mire kell, a vektor készen lesz!

Osztályszintű konstruktor belsejében nem jellemző hogy kivételt dobnánk, illetve olyan műveleteket végeznénk el, amely kivételt okozhat. Ugyanis nem tudhatjuk, hogy a konstruktor pontosan mikor is fog lefutni, így nem tudhatjuk, hova helyezzük el az esetleges kivételt kezelő kódot.

Hernyák Zoltán
A lap eredeti címe: „http://wiki.ektf.hu/wiki/Mp3/ea10
Nézetek
nincs sb_3.141.100.120 cikk