fel
le

Kötés

A fordítóprogram elsődleges feladata, hogy az általunk megírt forráskódot elolvassa, elemezze, megértse. Ezen elemzés közben listát vezet a kód olvasása során felismert változókról, konstansokról, típusokról, függvényekről. Az egyes programozási elemekhez rendelt nevek alapján képes felismerni melyik programsorban melyik, korábban definiált elemre hivatkozunk.

Kötés-nek nevezzük azt a folyamatot, amikor a fordítóprogram a forráskód fordítása során felfedezett metódushívásokhoz konkrét metódust rendel.

A kötés során döntést kell hoznia, hogy melyik metódushívás melyik metódust jelentse. Ennek szabályai vannak:

  • Overloading szabály: a hívás helyén feltüntetett aktuális paraméterlista melyik formális paraméterlistára illeszkedik.
  • Statikus típus szabály: amely példányon keresztül próbáljuk meghívni a metódust, annak a deklarált típusa határozza meg a kiválasztott metódust.

Példa az overloading-ra:

class TAuto
{
   public void Balkanyar()  { ... }
}

class TAuto2:TAuto
{
   public void Balkanyar(double szog)  { ... }
}

class FoProgram
{
   public static void Main()
   {
      TAuto2 kocsi = new TAuto2();
      kocsi.Balkanyar();
      kocsi.Balkanyar(45);
   }
}
 

A fenti példában az első Balkanyar() hívás egyértelműen a TAuto-ban definiált metódust jelent, míg a második, paraméteres Balkanyar(45) pedig a TAuto2-ben definiált metódust jelöli.

Példa a statikus típus szabályra:

class TRepulo
{
   public void Felszall()  { ... }
}

class THelikopter:TRepulo
{
   new public void Felszall() { ... }
}

class FoProgram
{
   public static void Main()
   {
      TRepulo f15 = new TRepulo();
      f15.Felszall();
      THelikopter apache = new THelikopter();
      apache.Felszall();
   }
}
 

Az első esetben az f15 példány típusa TRepulo, ezért a rajta keresztül meghívott metódus esetén a fordító megvizsgálja, hogy mely metódusok jöhetnek szóba. A TRepulo osztályban definiált Felszall() metódus nyilván szóba jöhet, míg a helikopter-é nem. A második esetben, az apache esetén pedig nyilván a new-val felüldefiniált új, helikopteres felszálló metódus kerül meghívásra.

Korai kötés

A kötés során metódushívást rendelünk metódushoz.

A korai kötés-ről akkor beszélhetünk, ha ez az összerendelés fordítási időben történik, a fordítóprogram által.

A korai kötés legfontosabb jellemzője, hogy oldhatatlan, megváltoztathatatlan kötés. A programozók egymás között szoktak úgy is fogalmazni: "beheggesztődik a kódba". Vagyis ha a fordítóprogram egyik kötés alkalmával meghozott egy konkrét döntést, akkor azt később már sosem fogja felülbírálni, megváltoztatni:

class TRepulo
{
   public void Felszall()  { ... }
   public void Leszall()   { ... }
   public void Repul()     { ... }
   
   public void Gyakorlokor()
   {
     Felszall();
     Repul();
     Leszall();
   }
}

class THelikopter:TRepulo
{
   new public void Felszall() { ... }
   new public void Leszall() { ... }
}

class FoProgram
{
   public static void Main()
   {
      TRepulo f15 = new TRepulo();
      f15.Gyakorlokor();
      THelikopter apache = new THelikopter();
      apache.Gyakorlokor();
   }
}
 

Gondoljuk végig, mely metódusok hívódnak meg az f15.Gyakorlokor() és az apache.Gyakorlokor() esetén!

A fordítóprogram fordítás a TRepulo osztály fordítása közben amikor a Gyakorlokor() metódus törzsét fordítja, ott metódushívásokat talál. Ezeket megpróbálja kötni valamely konkrét metódusokhoz. Mivel jelenleg a TRepulo objektumosztály fordítását végzi, és ebben megtalálja a keresett metódusokat, így ezen metódushívásokat rendre összeköti a TRepulo osztályban megtalálható metódusokkal.

A THelikopter osztály fordítása során kötési feladatokkal nem fog találkozni, hiszen ott csak metódusdefiníciók találhatóak, metódushívások nem.

A főprogram fordítása során kettő metódushívással találkozik, az f15 és az apache példányokon keresztül. Az f15.Gyakorlokor() kötése egyértelmű, a statikus szabály szerint ez a TRepulo osztálybeli Gyakorlokor() lesz. Hasonlóan egyértelmű a helyzet az apache esetén is, mivel a THelikopter-ben nem került felüldefiniálásra a Gyakorlokor() metódus, így marad az eredeti, örökölt metódus, a TRepulo-beli Gyakorlokor().

Vizsgáljuk meg hogyan működik a futó program! Amikor a főprogramban elérjük a f15 példány gyakorlókör metódushívását, egyszerűen követni kell a korai kötés döntéseit:

f15.Gyakorlokor()
 -> TRepulo -beli Gyakorlokor()
{
  Felszall()  -> TRepulo -beli Felszall()
  Repul()     -> TRepulo -beli Repul()
  Leszall()   -> TRepulo -beli Leszall()
}
 

Ez ideig nem okozott semmi váratlan viselkedést, hiszen az f15 példány a TRepulo-bol lett példányosítva, amelynek léteznek Felszall(), Repul(), Leszall() metódusai, és nyilván ezek kerülnek használatra.

Most ellenőrízzük ugyanezt az apache példány esetén:

apache.Gyakorlokor()
 -> TRepulo -beli Gyakorlokor()
{
   Felszall()     -> TRepulo -beli Felszall()
   Repul()        -> TRepulo -beli Repul()
   Leszall()      -> TRepulo -beli Leszall()
}
 

Vagyis azt tapasztaljuk, hogy a THelikopter-ben hiába került new segítségével felüldefiniálásra a Felszall() és Leszall(), nem kerültek meghívásra! Ez viszont fontos tanulság, és nem feltétlenül logikus!

Tanulság: a new-al felüldefiniált metódusok működnek, amennyiben direktben hívjuk meg őket a megfelelő típusú példányon keresztül, de az örökölt metódusokból nem lesznek elérhetőek. Az örökölt metódusok belsejében lévő metódushívásokra a korai kötés vonatkozik, amely egyrészt megváltozathatatlan, másrészt nem irányulhat egy későbbi osztály belsejében később definiált metódusok felé.

A new-val történő metódus-felüldefiniálás csak félig nyújt megoldást az objektum-osztályunk továbbfejlesztési problémáira. Az ilyen metódusokat a módosítás nélkül átvett, örökölt metódusok nem fogják felhasználni mint javított változatot. Ezek hiányában az osztályunk csak "félig javított" állapotú, ha egy ilyen örkölt metódust kívánunk meghívni, akkor ez a "felemás viselkedés" előjön, és ez komoly működési zavarokhoz vezethet.

Mi lehet a megoldás?

A korai kötés kezelése

A fenti esetben megoldást nyújthat az alábbi módosítás:

class TRepulo
{
   public void Felszall()  { ... }
   public void Leszall()   { ... }
   public void Repul()     { ... }
   
   public void Gyakorlokor()
   {
     Felszall();
     Repul();
     Leszall();
   }
}

class THelikopter:TRepulo
{
   new public void Felszall() { ... }
   new public void Leszall() { ... }
   new public void Gyakorlokor()
   {
     Felszall();
     Repul();
     Leszall();
   }
}

class FoProgram
{
   public static void Main()
   {
      TRepulo f15 = new TRepulo();
      f15.Gyakorlokor();
      THelikopter apache = new THelikopter();
      apache.Gyakorlokor();
   }
}
 

Ebben az esetben a TRepulo osztályban feltüntett Gyakorlokor() metódust egyszerűen lemásoltuk a THelikopter belsejében a copy-paste segítségével. Milyen változást eredményez ez?

A THelikopter fordítása során a new Gyakorlokor() metódusban megadott metódushívásokat a fordító megpróbálja kötni. A Felszall()-t a saját osztálybeli new Felszall()-hoz, a Repul()-t jobb híján az örökölt Repul()-höz. A Leszall()-t az új new Leszall()-hoz.

A főprogramban az apache.Gyakorlokor() metódushívás kötése során a fordító ehhez az új new Gyakorlokor() metódushoz fog kötni. Lássuk mi történik futás során:

apache.Gyakorlokor()
 -> THelikopter -beli Gyakorlokor()
{
  Felszall()     -> THelikopter -beli Felszall()
  Repul()        -> TRepulo -beli Repul()
  Leszall()      -> THelikopter -beli Leszall()
}
 

Vagyis a kód futás közben úgy működik, ahogy azt mi elvártuk volna. Node milyen áron?

A copy-paste módszerrel történő OOP kód fejlesztés teljesen hibás hozzáállás! Ez a kód duplikálását jelenti, hiszen a Gyakorlokor() metódus belseje a forráskód szerint (a kötésektől eltekintve) megegyezik az örökölt metódus törzsével. Ez pontosan az egyik OOP alapelv, az öröklődés elvének sárba tiprását jelenti! Ezen elv azért került kidolgozásra, hogy a fejlesztést meggyorsítsa, és azt mondja ki, hogy amennyiben egy metóduson nem végzünk módosítást, úgy azt vegyük át változtatás nélkül az őstől.

Gondoljuk ezen kívül végig az alábbiakat:

  • a metódus copy-paste átvétele nem minden esetben kivitelezhető, mivel leggyakrabban az ős osztály forráskódja nem is áll rendelkezésre
  • az eredeti metódus hivatkozhat private mezőkre, vagy hívhat private metódusokat. Ezt a copy-paste kód nem képes megtenni a gyermekosztályban
  • amennyiben lemásolnánk a metódust magunkhoz, úgy elvesztenénk a kapcsolatot az ős osztállyal ezen a vonalon. Ha az ős osztályban javítanak ezen a metóduson, akkor vagy újra meg kell ismételnünk a copy-paste műveletet, vagy beletörődünk, hogy a mi felüldefiniált metódusunk marad a régi, kicsit hibás.

Nyilván a fentiek egyike sem engedhető meg, ha jó minőségű kódot szeretnénk írni. Olyan megoldást kell keresni, amely megoldja a fenti problémákat!

Hernyák Zoltán
2013-03-17 19:17:04