fel
le

Interface

Az öröklődés, és ezzel kapcsolatosan a típuskompatibilitás kapcsán volt már szó olyan technikákról, amelyek segítségével próbálunk különböző objektumosztályokat egyforma szemmel nézni és kezeleni. Ennek során megállapítottuk, hogy kereshető olyan típus, amellyel annak gyerekosztályai mindíg kompatibilisek, és ha olyan metódust fejlesztünk, amely ilyen típusú paramétert fogad - akkor minden gyerekosztályának példányait is képes az fogadni és kezelni.

Ez a szemlélet nagyon hasznos, mivel a típusbiztonságot a fordítóprogram ellenőrzi, és kódolás szempontjából pedig nagyon hatékony megoldás. Ezt a szemléletet próbáljuk kiterjeszteni a következőkben is.

Tegyük fel, hogy szeretnénk egy olyan univerzális rendezőalgoritmust kifejleszteni, amelynek átadható több alaptípusra épülő vektor, és ezen rendezőalgoritmus mindegyikkel képes lesz dolgozni. Mit igényel ez a rendezőalgoritmus? A vektor alaptípusára végrehajtható legyen az összehasonlítás valamilyen formában, vagyis eldönthető legyen két ilyen alaptípusú példányról, hogy melyik a kisebb, melyik a nagyobb, illetve egyenlők-e. Másra gyakorlatilag nincs is szükség.

Ezen összehasonlítást általában operátorokkal szoktuk megoldani (</b>, <=, , , ==), de tegyük fel hogy nem kérjük ezen típusra az összehasonlító operátorok kidolgozottságát, mindössze annyit kérünk, hogy rendelkezzen ezen alaptípus egy CompareTo(...) függvénnyel, melyen keresztül le tudjuk kérdezni az összehasonlítás eredményét:

class AlapTipus
{
    // térjen vissza -1  -el,  ha this < b
    // térjen vissza  0  -val, ha this == b
    // térjen vissza +1  -el,  ha this > b
    public int CompareTo( AlapTipus b )
    {
       ...
    }
}
 

Igazából minden olyan objektumosztállyal tud a rendezőalgoritmus dolgozni, amely rendelkezik ilyen nevű, paraméterezésű, és viselkedésű metódussal. De melyik típussal írjuk ezt le? Létrehozhatnánk egy Rendezheto nevű absztrakt osztályt, amely tartalmazza ezt a metódust, és rávehetnénk minden objektumosztályt, amely használni akarja a fenti metódust, hogy legyen ezen osztály gyerekosztálya:

// az alaposztály
abstract class Rendezheto
{
  public int CompareTo( Rendezheto b );
}

// a metódus váza
public static Rendezes( Rendezheto[] adatok )
{
  ..
}

// például a String osztály
class String:Rendezheto
{
  public override int CompareTo( String b )
  {
    ... a kód ...
  }
}
 

A fenti esetben például egy String[] vektor esetén használhatnánk a Rendezes metódust, hiszen a paraméterezése kompatibilis.

De ez a kérés nagyon nehezen terjeszthető ki széles körre. Ha ugyanis az az objektum-osztály, amelyet ősként szeretnénk választani - nem a Rendezheto osztály gyermekosztálya, akkor a mi fejlesztendő osztályunk már nem is válhat azzá. Például tegyük fel, hogy a Kor osztályból szeretnénk származtatni egy Ellipszis osztályt, de a Kor osztály ősei között nem szerepel a Rendezheto. Ugyanakkor szeretnénk az Ellipszis-ekből egy vektort készíteni, és a fenti hatékony rendezőeljárás segítségével rendezni azokat. Mi szivesen írnánk ilyen CompareTo metódust az Ellipszis osztályunk belsejébe, mondjuk azon szempont szerint, hogy az ellipszisek területe lenne a kisebb-nagyobb-egyenlő kérdés döntő szempontja. De hiába tennénk ezt meg - a Rendezes eljárás nem fogadná paraméterként az Ellipszis[] vektort, mivel annak típusa nem kompatibilis a Rendezheto osztállyal. Ráadásul egyetlen osztálynak csak egyetlen őse lehet, tehát nem írhatjuk le az Ellipszis osztályhoz, hogy őse a Kor és őse a Rendezheto osztály is.

Interface

Igazából tehát a típuskompatibilitás nagyon jó dolog, de nem ad minden problémára megoldást. Nagyon gyenge kiterjeszthetősége, hiszen egy osztálynak csak egy őse lehet. Ugyanakkor a fenti esetben ez egyszerűen túl nagy kérés a szóban forgó alaptípustól. Nem kívánjuk valójában hogy őse legyen ez a bizonyos Rendezheto osztály, csak azt kívánjuk, hogy legyen ilyen CompareTo(...) metódusa.

A fenti problémát helyesen nem is így kell kezelni. Nem abstract osztályt kell létrehozni erre a célra, hanem ennek gyakorlatilag a mását - egy interface-t:

// az alaposztály
interface IRendezheto
{
  int CompareTo( Rendezheto b );
}

class String:IRendezheto
{
  public override int CompareTo( String b )
  {
    ... a kód ...
  }
}
 

Az interface az gyakorlatilag nem más, mint egy teljesen letisztult absztrakt osztály. Egy ilyen interface tartalmazhat:

  • metódus fejrészét: visszatérési érték típusa, metódusnév, formális paraméterlista
  • property: a property típusát, nevét, és a get és set részek jelölését
  • indexelő property: hasonlóan a közönséges property-hez

Mit nem tartalmazhat egy interface:

  • metódus törzset: csak a metódusok fejrészét kell leírni
  • a metódusok nem jelölhetők meg védelmi szintekkel (public, protected, private)
  • a metódusok nem jelölhetőek meg abstract sem virtual sem override jelzésekkel
  • a property-k hasonlóan nem jelölhetőek védelmi szintekkel, sem egyéb módosítókkal
  • nem tartalmazhat mezők deklarációját!
  • nem tartalmazhat sem konstruktort, sem destruktort

Az interface ennek megfelelően hasonlít egy absztakt osztályra, de valójában esélytelen tőle érdemi dolgot örökölni. Csakis fejrészeket tartalmazhat, kódot és mezőket semmilyen formában nem.

Ugyanakkor az interface is típusnak minősül, ennek megfelelően lehet ilyen típusú változókat,paramétereket deklarálni:

// a metódus váza
public static Rendezes( IRendezheto[] adatok )
{
  ..
}
 

De ki lehet kompatibilis egy interface típussal? Hát továbbra is az a válasz, hogy aki ősnek választja!

Minden objektumosztálynak csak egy közvetlen másik objektumosztály-őse lehet. De mindegyiknek lehet számtalan másik interface őse!

class Ellipszis: Kor, IRendezheto
{
  ..
}
 

Amennyiben egy osztály őseként megad egy interface-t, azzal azt a "kötelezettséget" vállalja magára, hogy az interface-ben megjelölt metódusok és propertyk mindegyikét kidolgozza. Amennyiben egyről is "elfedkezne", a forditóprogram keményen figyelmezteti az osztályt (szintaktikai hiba), és megtagadja annak lefordítását!

Amennyiben egy osztály őseként választ egy interface-t, és megvalósítja az abban deklarált összes metódust, property-t, indexelőt, akkor azt mondjuk, hogy az osztály implementálta az adott interface-t.

Ha egy osztály őséül jelöl meg egy interface-t, akkor ezzel annak implementációját vállalja magára. Ha valamely, az interface-ben jelölt metódust, property-t, indexelőt nem dolgozna ki, akkor azt a fordítóprogram hibának tekinti, és megtagadja az adott osztály fordítását. Ekkor az alábbiak közül választhatunk:

  • kidolgozzuk a hiányzó elemet is, és az interface implementálását befejezzük.
  • eltávolítjuk az interface-t az ősök listájáról.
  • a hiányzó metódust, indexelőt vagy property-t csak abstract formában vesszük be az osztályba. Ekkor az implementálást a gyerekosztály nyakába "sózzuk". Ekkor természetesen az osztály maga is absztrakt jelzésűvé kell váljon.

Ha egy osztály valamely interface-t implementálni szeretne, és az abban jelölt metódusokat, property-ket, indexelőket már eleve tartalmazza, vagy az ősosztályától azokat eleve örökölte, akkor a kötelezettségeket eleve teljesíti. Az interface implementációja mindössze annyit ír elő, hogy az adott osztály tartalmazza a szóban forgó elemeket, de ez a tartalmazás oly módon is teljesítettnek tekintendő, hogy ezeket az elemeket (vagy azok egy részét) az ős osztálytól öröklés miatt tartalmazza.

Az interface-ben deklarált metódusok, indexelők, property-k az implementáló osztályban

  • kötelezően public védelmi szinttel kell rendelkezzenek
  • hogy virtuális (késői kötésű), vagy egyszerű (korai kötésű) módon kerülnek kidolgozásra, arról az interface nem rendelkezik külön.

Interface-k öröklődése

Az interface tehát típusnak minősül, és ugyanazon típuskompatibilitási szabály vonatkozik rá, mint a teljes értékű osztályok esetén: az interface-t implementáló (az interface gyerekosztályai) osztályok típuskompatibilisek a szóban forgó interface-el. Ennek megfelelően interface-ek esetére is működik az is és as operátorok a megfelelő módokon.

Az interface-ek nem csak ebben hasonlítanak az osztályokra, hanem abban is, hogy az interface-eknek is lehet ősük. Egy interface-nek azonban csak másik interface lehet őse, osztály nem.

interface IRajzolhato
{
  void Kirajzol();
  void Letorol();
  int xKoord
  {
    get;
    set;
  }
}

interface IFestheto:IRajzolhato
{
  Color kifestoSzin
  {
    get;
    set;
  }
}
 

A fenti példában az IFestheto interface őse az IRajzolhato interface. Az öröklődés itt is hasonlóan működik mint osztályok esetén, tehát az az osztály, aki implementálni szeretné az IFestheto metódust, annak ki kell dolgoznia a Kirajzol(), a Letorol() metódusokat, az Xkoord property-t, és a kifestoSzin property-t is egy időben. Ugyanakkor ha ennek eleget tesz, akkor egyszerre lesz kompatibilis az IFestheto és az IRajzolhato interface-el is!

Interface és az Object

Mivel az interface-el gyakorlatilag új típust hozunk létre, felmerül ezen új típus kompatibilitási problémája az Object típussal.

A kérdés körbejárása nem egyszerű. Ugyanis az interface őse nem lehet objektumosztály, és az Object az egy class, vagyis objektumosztály. E miatt megdőlni látszik az a korábbi kijelentés, hogy a C#-ban minden létező típus valamilyen szinten kompatibilis az Object típussal.

Ez a kijelentés ebben a formában igaz, az interface típusoknak nem őse az Object, e miatt nem kompatibilisek vele.

Másrészről azonban ez a kérdés gyakorlatilag nem merül fel semmilyen formában. Ugyanis az interface típusból példányosítani nem lehet, hiszen ő nem tartalmaz konstruktort, és amúgy sem minősül objektumosztálynak. Példányosítani csak osztályokból lehet. Ha ezen osztály kompatibilis egy interface-el (implementálta azt), akkor bekerülhet a példánya egy ilyen interface típusú változóba:

public void Mozgatas_X(int eltolas_X, IRajzolhato g)
{
  g.Letorol();
  g.xKooord += eltolas_X;
  g.Kirajzol();
  if (g is Ellipszis) (g as Ellipszis).Sugar2--;
}
 

Ezen metódus meghívása olyan példánnyal lehetséges, amely egy olyan objektumosztály példánya, amely implementálta az IRajzolhato interface-t. Ekkor az if (g is Ellipszis) vizsgálat során nem azt teszteljük, hogy az IRajzolhato kompatibilis-e az Ellipszis osztállyal, hanem a g változóban lévő példány dinamikus típusa kompatibilis-e ezzel az osztállyal. Tehát magának az interface-nek mint típusnak a kompatibilitási kérdései nem merülnek fel.

Interface származtatás

Egy interface-nek nem lehet objektum-osztály őse, mivel akkor örökölhetne, s így tartalmazhatna mezőket, kidolgozott metódusokat, stb. Azonban interface őse lehet másik interface, így az interface-k között is értelmezve van a gyermek-ős viszony.

Itt is érvényben van, hogy a gyermek-interface az ősétől örökli az abban deklarált metódusok, property-k, indexelők a gyermek-interface-be is belekerülnek deklarációs szinten. Vagyis ha valamely osztály ezen gyermek-interface-t próbálja implementálni, akkor ezeket a property-ket, indexelőket, metódusokat is implementálnia kell.

Egy interface-nek több más interface is lehet egyszerre őse.

interface IBeallithato
{
    string ertek
    {
        get;
        set;
    }
}

interface IKiirhato
{
    void Kiiras();
}

interface INovelheto : IBeallithato, IKiirhato
{
    void Noveles();
}
 

A fenti példának megfelelően amely osztály implementálni szeretné az INovelheto interface-t, annak implementálnia kell az ertek property-t, a Kiiras() és Noveles() metódusokat.

Interface fogalmát nem ismerő OOP nyelvek

Egyetlen objektumosztálynak csak egyetlen másik objektumosztály lehet az őse, de több interface-t is implementálhat egy időben:

class Ellipszis: Kor, IKirajzolhato, IRendezheto, IArchivalhato
{
   ...
}
 

Más OOP nyelveken az interface fogalma teljes mértékben hiányzik. Helyette azonban ezen nyelveken egyetlen objektumosztálynak több őse is lehet. Itt az interface helyett valós absztrakt osztályokat szoktak készíteni, és az tölti be az interface szerepét - vagyis hogy az amúgy különböző osztályok közötti hasonlóságot leírja. Ilyen nyelv pl. maga a C++.

Hernyák Zoltán
2013-03-17 19:42:38