fel
le

Konstruktorok

A konstruktorok az osztálypéldány inicializásáért felelős metódusok. Védelmi szintjeik ennek megfelelően szintén public, protected és private lehetnek. Bár leggyakrabban egyedül állnak, sosem vannak egyedül. A példányosításkor szinte sosem egyetlen konstruktor fut le - jellemzőbbb a sok konstruktoros eset.

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, double d) { ... }
    public C(int f):this(f,10.2) { ... }
 }
 

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.

1. ábra: konstruktor hívási lánc

A példányosítás során a fejlődési fa alsó szintjén lévő saját osztályunkból (az ábrán az (5) sorszámú) példányosítunk. Az (5) ősosztályából (4) is kiválasztódik egy konstruktor, ahogy annak ősosztályából is. A bal oldali nyilak mutatják, ahogy a kostruktorok összekapcsolódnak. A jobb oldali nyíl szerint azonban a lefutás felülről-lefele történik meg, először a legősibb soztály konstruktora (1) fut le, majd lefele haladva a többi.

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.2)' 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.2)
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, double d) { ... }
    public beta(int f):this(f,10.4) { ... }
 }
 
 ...
 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 explicit módon 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, double d):base(f) { ... }
    public beta(int f):this(f,10.4) { ... }
 }
 
 ...
 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() { }
}
 
2. ábra: konstruktor hívási lánc

A példányosítás során a fejlődési fa alsó szintjén lévő saját osztályunkból (az ábrán az (5) sorszámú) példányosítunk. Ezen szinten is több konstruktor is lehet. A példányosítás során feltüntetett paraméterezés alapján a fordítóprogram azonosítja a megfelelő konstruktort. Ezen konstruktorhoz tartozó this, base alapján a fordító tud választani, hogy melyik másik konstruktort hívja meg ez előtt. Ezt a lépéssort ismételve a fordító azonosítja az (5) előtt lefutó összes konstruktort alulról felfele haladva a fejlődési fában. A folyamat a legfelsőbb szinten (Object) akad el. Ez a szint ahol egyedülállóan vannak olyan konstruktorok, amelyekhez nem tartozik base.

Ez a folyamat (valamely konstruktorhoz meghatározni az előtte lefutó konstruktort) valójában nem a példányosításkor történik meg, hanem amikor az adott osztályokat, a konstruktorokat megírtuk. A konstruktor megírásakor a fordító ellenőrzi, hogy melyik elő-konstruktor tartozik hozzá. Ha ilyet nem találna, akkor követeli hogy a base segítségével mi adjuk meg explicit módon. A példányosításkor csak a legalsó szint megfelelő konstruktorát választjuk ki a paraméterezés révén explicite, s ezzel valójában a felette lévőket is implicite, vagyis a teljes láncot felfele haladva az Object-ig.

A kiválasztott lánc lefutása felülről lefele történik meg. Legelőször a legfelső (1) szinten lévő legelső konstruktor fut le, majd lefele haladva a többi is. Legutóljára fut le a példányosításkor a paraméterezéssel kiválasztott konstruktor.

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();
 

Singleton

A singletonok olyan objektumok, amelyekből futási időben csak egyetlen példány készülhet. Persze felmerül a kérdés, hogy ha csak egy példány készül, azt akkor miért nem oldjuk meg példány nélkül osztályszintű metódusokkal és mezőkkel, amelyek hasonló viselkedésűek, de nyilván ennek is oka van, számtalan, de ebbe most nem kívánunk belemenni, helyette inkább a megvalósításra koncentráljunk: hogyan oldjuk meg, hogy ne lehessen több mint egy példányt készíteni az osztályból. Ha publikus konstruktort készítünk - akkor a külvilág tetszőlegesen sok példányt készíthet.

class MySingleton
{
    protected MySingleton()
        {  
          ...
        }
   
        protected MySingleton pld = null;

    public static MySingleton Letrehoz()
    {
       if (pld==null)
             pld = new MySingleton();
           
       return pld;
    }
}  
...

MySingleton p1 = MySingleton.Letrehoz();
MySingleton p2 = MySingleton.Letrehoz();
 

A p1 példány készítésekor (első lefutás) a példány a protected konstruktora segítségével ténylegesen létrejön, és a pld protected mezőbe eltárolásra kerül a referenciája. A p2 példány készítésekor már újabb példány nem készül, a korábban létrejött példány referenciája kerül függvényérték formájában visszaadásra.

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 ProbaB osztályból, akkor a konstruktor hívási lánc működésének megfelelően a 'ProbaA' 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 ProbaA konstruktor belsejében lévő 'Kiiras()' függvényhívás már az új verzió, a ProbaB 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 ProbaA-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
2013-06-17 04:34:23