fel
le

Típuskompatibilitás

A típuskompatibilitási szabály azt mutatja meg, hogy két típus (két objektumosztály) mikor kompatibilis egymással. A szabály megalkotása előtt idézzük fel az öröklődésről tanultakat: egy gyermekosztály örökli ősének minden mezőjét, metódusát, property-jét. Ez azt is jelenti, hogy a gyermektípus "mindent tud", amit az őse tud, sőt, valószínűleg még többet is, hiszen új mezőket, metódusokat fejleszthetünk, vezethetünk be.

A típuskompatibilitási szabály szerint a gyermekosztály mint típus kompatibilis az ős osztályával, mint típussal. A típuskompatibilitás egyirányú, vagyis a gyermek kompatibilis az ősével, de az ős nem kompatibilis a gyermekosztállyal.

A típuskompatibilitás működése

Ha egy B típus kompatibilis egy A típussal, akkor egy A típusú változó felvehet egy B típusú értéket.

Nézzünk erre példákat:

class Negyzet
{
   public int AOldal;
   public virtual int kerulet()
   {
      return AOldal*4;
   }
   public int terulet()
   {
      return AOldal*AOldal;
   }
}

class Teglalap: Negyzet
{
   public int BOldal;
   public override int kerulet()
   {
      return (AOldal+BOldal)*2;
   }
   new public int terulet()
   {
      return AOldal*BOldal;
   }
}
// ...........
Negyzet a = new Negyzet();
a.AOldal = 12;
Teglalap b = new Teglalap();
b.AOldal = 14;
b.BOldal = 23;
a = b;
 

Figyeljük meg a legalsó értékadó utasítást. A bal oldalon álló változó (a) típusa Negyzet. Az értékadó utasítás jobb oldalán álló kifejezés típusa Teglalap. Az értékadó utasítás szabálya szerint a jobb oldal típusának vagy egyformának, vagy kompatibilisnek kell lennie a bal oldal típusával, különben az értékadó utasítás típushelytelen.

Mivel a Teglalap típus a Negyzet gyermekosztálya, így típuskompatibilis vele. Ennek megfelelően a fenti értékadó utasítás (a=b) típushelyes.

De mi lesz ennek a következménye?

Mellékesen megjegyeznénk, hogy a létrehozott Negyzet típusú példány (AOldal=12) egyetlen referenciája, mely az a változóban volt tárolva korábban - elveszett. Ekkor a Garbage Collector ezt a felesleges példányt törli a memóriából.

Maradt egyetlen Teglalap típusú példádnyunk a memóriában, melynek referenciáját két változó is őrzi: a is és b is. Mindkét változó ugyanazt a referenciát jelöli. Folytassuk tovább a fenti egyszerű kis programot:

...
a = b;
a.AOldal = 20;
Console.WriteLine(b.AOldal);
 

A képernyőn a 20 érték fog megjelenni, mivel mindkét változónk ugyanarra a Teglalap példányra mutat.

...
a = b;
a.AOldal = 20;
Console.WriteLine(a.terulet());
Console.WriteLine(b.terulet());
 

Az első kiírás a 400 értéket írja ki, mivel 20*20=400. Az terulet() függvény nem virtuális, meghívásakor (a.terulet()) a korai kötés szabályai működnek. Az a változó deklarált (statikus) típusa Negyzet, vagyis a Negyzet -> terulet() függvény fog meghívódni.

A második kiírás a 460-t fogja kiírni, mivel korábban a BOldal-ba 23-t raktunk, és 20*23=460. Látszik, hogy bár itt is a korai kötés működik, de a b statikus típusa Teglalap, így értelemszerűen a Teglalap -> terulet() fog meghívódni.

...
a = b;
a.AOldal = 20;
Console.WriteLine(a.kerulet());
Console.WriteLine(b.kerulet());
 

A képernyőn mindkét alkalommal a (20+23)*2, vagyis a 86 érték fog kiíródni. Miért? Mivel a kerulet() függvényünk virtuális, így az a.kerulet() hívás a VMT tábla alapján fog kiértékelődni. Márpedig az a változóban tárolt példányhoz a Teglalap osztály VMT táblája tartozik, így a Teglalap->kerulet() függvény fog meghívódni (mindkét esetben).

...
a.BOldal = 30;
 

A fenti sor azonban szintaktikai hibás. Ugyanis az a változónk statikus típusa Negyzet. Márpedig egy négyzetnek nincsen BOldal mezője, így az a változónak sincs ilyen nevű mezője. A fordítóprogram a fenti esetben nem mérlegeli, hogy egy korábbi értékadó utasítás során az a változóba gyakorlatilag nem egy Negyzet, hanem egy Tegalalp osztálybeli példány került-e bele vagy sem. Az a változón keresztül a Teglalap osztály speciális, új fejlesztésű mezői és metódusai nem érhetőek el. A '.' (pont) operátor, a mező- és metóduskiválasztó operátor minden esetben a változó (példány) statikus típusa alapján működik.

Foglaljuk össze:

  • a korai kötés a statikus típus alapján működik
  • a pont operátor a statikus típus alapján dönti el, hogy van-e adott mező vagy metódus része a példánynak
  • a késői kötés azonban jól működik az a=b értékadás után is (VMT tábla alapú)

A típuskompatibilitás használata (1)

A fenti példa, amikor direktben adunk egy ős-típusú változónak értéket egy gyermek-típusú példánnyal - nagyon ritka. Alkalmas arra, hogy átmenetileg tárolja a gyermek-példány referenciáját, de ritkán tesszük bele a referenciát azért, hogy így müködtessük a példányt, így hivogassuk a függvényeit, használjuk a mezőit. Gondoljunk csak arra, hogy az összes mezőt el sem érjük, csak azokat, amelyeket az ős osztály is tartalmaz, illetve a függvények közül csak a virtuálisok működnek garantáltan jól.

Ugyanakkor a fenti jellegű értékadás és az utána történő működés megértése kulcsfontosságú! Ennek hiányában nem fogjuk érteni az alábbi kód működését:

public static void Kiprobal(Negyzet x)
{
  x.AOldal = 20;
  Console.WriteLine(x.kerulet());
  Console.WriteLine(x.terulet());
}
//....
Kiprobal( a );
Kiprobal( b );
 

A fenti eset már nagyon is gyakori az OOP kódolás világában. A Kiprobal() függvény paramétertípusa Negyzet. A híváskor ennek megfelelően egy Negyzet típusú (vagy vele kompatibilis) típusú értéket kell megadni. Ennek a szabálynak nyilván megfelel a Kiprobal(a) függvényhívás, hiszen az a típusa is Negyzet. De a típuskompatibilitási szabály miatt jó lesz a 'Kiprobal(b)' függvényhívás is! Ugyanis a 'b' típusa kompatibilis az elvárt 'Negyzet' típussal!

A függvényhívás pillanatában a formális paraméterek rendre felveszik a híváskor megadott értékeket. Vagyis első esetben az 'x=a' értékadás zajlik le a színfalak mögött. Ez nyilván típusilag teljesen rendben lévő értékadás, semmilyen gond nincs vele.

A második függvényhívásunk esetében az 'x=b' értékadás hajtódik végre. Mint láttuk, ez is típusilag korrekt értékadás, értelme, és működése van feljebb kivesézve.

A típuskompatibilitás használata (2)

Nézzük csak meg mit kaptunk: egy olyan függvényt kaptunk, amely paraméterként egy 'Negyzet', vagy azzal kompatibilis típusú példányt vár, ezen példánynak beállítja az 'AOldal' mezőjének értékét, és kiírja a kerület és terület értékeket a képernyőre.

A függvény beljesébe ha benézünk, akkor látjuk, hogy a függvény erősen kihasználja azt, hogy a kapott 'x' példánynak van 'AOldal' mezője, és 'kerulet()' és 'terulet()' függvénye. Mely típusoknak van ilyenjük garantáltan? Hát a 'Negyzet' osztálynak magának, és minden gyermekosztályának is (öröklődés útján beszerezve).

Ezen ismeret és módszer segítségével fejleszthetünk olyan általánosabb függvényeket, melyek sokféle osztály példányával paraméterezhető, és képes azokat az adott szinten összefogni, működtetni. Ezen módszer azt követeli meg, hogy ezen példányok osztálya mind a paraméter-típusként megjelölt osztály gyermekosztályaiból készüljenek. Ez persze erősen korlátozza azért a kezelhető osztályok számát. Ezt a módszert később majd továbbfejlesztjük és általánosítjuk az 'interface' kapcsán.

A típuskompatibilitás további érdekességei

Negyzet n;
Teglalap t = new Teglalap(10,20);
n = t;
 

A fenti értékadás ('n=t') a típuskompatibilitás miatt műküdik. Az 'n' változó felveszi egy "telivér" téglalap példány referenciáját. Ezen példányhoz a téglalap osztály VMT táblája tartozik, így működnek vele kapcsolatosan a késői kötések is.

De ekkor működnie kell az alábbi kódnak is:

Negyzet n = new Teglalap(10,20);
 

Ebben a kódban nem először létrehozunk egy 'Teglalap' osztályú példányt, majd átcsempésszük a referenciáját az 'n' változóba, hanem rögtön oda helyezzük azt el. A bal oldal típusa ebben az értékadó utasításban 'Negyzet', a jobb oldal típusa 'Teglalap', de a típuskompatibilitás miatt ez helyes értékadó utasítás.

Lássuk mit kaptunk: ugyanazt, mit az előzó kódrészletben. Vagyis 'n' változónk egy "telivért" téglalap példány referenciáját hordozza, annak VMT-jével, működik rá a késői kötés, stb. A VMT táblát ugyanis a konstruktor rendeli a példányhoz, és a fenti kódban a 'Teglalap(..)' osztály konstruktora van használatban, vagyis ennek a VMT-jét tartalmazza a példány.

Ilyet nem gyakran csinálunk, hogy deklarálunk egy változót valamely típusra, majd egy másik típus konstruktora alapján készített példányt helyezünk el benne. Érdekesebb a helyzet akkor, ha ezt az alábbi módon végezzük el:

public static Negyzet Letrehoz(char tipus)
{
Negyzet ret = null;
switch(tipus)
{
  case 'N': ret = new Negyzet(10);break;
  case 'T': ret = new Teglalap(10,20);break;
  case 'K': ret = new Kocka(10);break;
  case 'L': ret = new Teglatest(10,20,30);break;
}
return ret;
}
 

A fenti esetben egy object-factory megvalósítást látunk, aki a megkapott paraméter értékének függvényében más-más osztályok példányát készíti el, és adja vissza. Amennyiben a fenti kódban szereplő osztályok mindegyike a 'Negyzet' osztállyal kompatibilis, úgy a fenti kód hibátlan, és működőképes.

Tranzitivitás

A típuskompatibilitás kimondja, hogy ha egy 'C' osztálynak egy 'B' osztály őse, akkor a 'C' osztály kompatibilis a 'B' osztállyal.

Ha a 'B' osztály őse valamely 'A' osztály, akkor a 'B' típuskompatibilis az 'A' típussal.

De mi a helyzet a 'C' és az 'A' kompatibilitásával?

class A_oszt { ... }
class B_oszt: A_oszt { ... }
class C_oszt: B_oszt { ... }
// ......
A_oszt a;
B_oszt b;
C_oszt c = new C_oszt(),
b=c;
a=b;
 

A fenti esetekben a 'b=c' típusilag helyes értékadás, hiszen a 'c' változó típusa kompatibilis a 'b' típusával. Hasonlóan az 'a=b' is helyes. De ha belegondolunk, a fenti kódban két lépésben bár, de valójában 'a=c' van írva. Kérdés: írhatunk-e közvetlenül 'a=c'-t?

A válasz igen. Az 'a=c' értékadás helyes! Ugyanis a típuskompatibilitás tranzitív tulajdonság, ha C kompatibilis B-vel, és B kompatibis A-val, akkor C kompatibilis A-val.

Vagyis egy gyermekosztály nem csak a közvetlen ősével kompatibilis mint típus, hanem annak őseivel is.

Közös ős

Amikor egy objektum-osztályt fejlesztünk, akkor két eset lehetséges:

  • megjelelöljük az általunk választott őst
  • nem jelölünk meg senkit ősnek

Ez utóbbi eset speciális módon van kezelve. Amikor nem jelölünk meg őst, akkor a fordítóprogram jelöl ki egyet, méghozzá ő minden esetben egy 'Object' nevű objektumosztályt jelöl meg ősnek.

class SajatOsztaly
{
 ...
}
 

A fenti kódban a 'SajatOsztaly'-nak nem jelöltünk ki őst, ez ekvivalens azzal, mintha az alábbit írtuk volna:

class SajatOsztaly:Object
{
 ...
}
 

Ebből következik, hogy minden egyes objektumosztálynak az 'Object' osztály van közvetlenül, vagy közvetve, de őse. Márpedig minden osztály minden ősével kompatibilis, nemcsak a közvetlen ősével. Vagyis minden osztály kompatibilis az 'Object' osztállyal.

Ez igen fontos következményekkel jár:

  • ha van egy Object típusú változónk, akkor abba "bármilyen" más típusú értéket el tudunk helyezni (ideiglenes megőrzésre).
  • ha egy függvény paramétere Object típusú, akkor a hívás helyén bármilyen típusú érték szerepelhet.

Ilyen függvény például az ArrayList osztály Add() függvénye:

class ArrayList
{
   public void Add(Object x)
   {
      ...
   }
}
// .......
ArrayList l = new ArrayList();
l.Add( "Hello" );
Kacsa k = new Kacsa();
l.Add( k );
...
 

Nyilván ennek az az oka, hogy maga az ArrayList gyakorlatilag nem is csinál semmi mást a megkapott referenciákkal, mint őrzi őket. Lekéréshez az indexelőt használhatjuk, de az indexelőnek is Object a visszatérési értéke:

class ArrayList
{
   public Object this [int i]
   {
      get { return 'i.eleme a listának';}
   }
}
// .......
ArrayList l = new ArrayList();
Kacsa k = new Kacsa();
l.Add( k );
...
Kacsak t = (Kacsa)l[0];
 

Megjegyzés: a listához value type típusú értékek is hozzáadhatók, ugyanis ezen típusok is kompatibilisek az Object alaptípussal. Ugyanakkor itt egyéb kérdések is felmerülnek, melyet a "boxing-unboxing" témakörben járunk körbe.

Hernyák Zoltán
2013-03-17 19:36:53