Személyes eszközök
Keresés

 

A InfoWiki wikiből


Tartalomjegyzék

Reflection használata, plugin

Jelen fejezetben a C# és a .NET Framework egy igen hasznos részterületével ismerkedünk meg: ez a 'System.Reflection' névtér tartalma.

Az ismert, hogy a .NET alatt íródott DLL-eket "assembly"-knek, szerelvényeknek nevezzük. Ilyen szerelvényeket a program fejlesztése közben csatolhatunk a projectünkhöz az "Add Reference" menüpont segítségével, és a fejlesztés közben az ezekben fellelhető publikus osztályokat felhasználhatjuk, azokból példányosíthatunk, azokat továbbfejleszthetjük, stb. Amikor a programunk elindul, az operációs rendszer ezen szerelvényeket megkeresi és betölti a memóriába. Bármelyik hiánya esetén a programunk futását még annak tényleges elindulása előtt leállítja, ellehetetleníti.

Sokkal problémásabb a helyzet akkor, ha olyan assembly-t kívánunk a programban használni, amelyet a fejlesztés során nem csatoltunk be. Először is értelemszerűen az ezekben lévő osztályokra a fejlesztés során nem hivatkozhatunk semmilyen módon, hiszen a fordító nem tudná ezek azonosítóit felismerni. Másrészt ezen assembly-k hiányában a programunk elindul, hiszen nem szükséges feltétel a futáshoz ezen assembly-k jelenléte, ezek nem regisztrált részei a programunknak.

Miért tennénk ilyet, hogy olyan assembly-kre hivatkozunk, melyeket nem csatoltunk hozzá a programhoz? Többek között azért, mert a fejlesztéskor ezek még nem voltak készen, vagy nem álltak rendelkezésünkre, illetve nem akarjuk ezt ilyen erős kötéssé alakítani. Erre legjobb példa a közismert WinAMP program, amelyhez számtalan kiegészítés (plugin) készül független fejlesztőktől, amelyek különféle szolgáltatásokat addnak hozzá a lejátszóhoz. Maga az mp3 file lejátszása is egy dinamikusan cserélhető plugin segítségével történik, nem is szólva a vizuális effektekről, stb.

Nyilván a plugin fejlesztése során bizonyos szabályokat be kell tartani, mivel ennek hiányában a program vázát alkotó alkalmazás (.exe) nem képes felismerni a plugin-t rejtő dll tartalmát. Megállapodás lehet az, hogy a pluginnek tartalmaznia kell egy bizonyos nevű osztályt, abban bizonyos nevű és paraméterezésű metódust. Amennyiben igen, úgy a program-váz futás közben ezen assembly-t képes betölteni a memóriában, megkeresni benne ezen megállapodás szerinti függvényt, és meghívni azt amikor annak szolgáltatására szükség van. Sajnos arra nincs lehetőség, hogy a program-váz megállapítsa, hogy az adott nevű függvény valóban azt a szolgáltatást is fogja nyújtani, amit elvárnak tőle, ezért ez bizalmi kérdés a két kód között.

Assembly betöltése dinamikusan

Az assembly-k dinamikus kezelését a 'System.Reflection' névtérben lévő osztályok, és azok metódusai végzik. Az alábbiakban megadunk egy olyan függvényt, amely egy assembly-t tölt be a memóriába annak fileneve ismeretében:

using System.Reflection;
using System.IO;
 
class PluginManager
{
    public static string pluginRootDir = @"C:\myProgram\myPlugins";
 
    public static Assembly LoadAssembly(string dllName)
    {
        // ha a dllName név vége nem .dll, akkor kiegészítjük
        if (Path.GetExtension(dllName).ToUpper()!="DLL") dllName+=".dll";
 
        // elébe írjuk a plugin-eket tartalmazó alkönyvtár nevét
        // (abszolút file név készítése)
        string dllFileName = Path.Combine(pluginRootDir, dllName);
 
        // a dll file betöltése, referencia elkészítése
        // ha a dll file betöltése nem sikerül, az 'a' értéke marad 'null'
        Assembly a = null;
        try { a = Assembly.LoadFile(dllFileName); }
        catch {}			
 
        // visszatérési érték
        return a;
    }
}

Amennyiben nem határozzuk meg a Assembly.LoadFile metódusnak a dll fileunk teljes elérési útvonalas nevét, úgy ő a megadott nevű dll filet valamennyi heurisztika szerint próbálja megkeresni. Keresi a futó program aktuális alkönyvtárában (.exe), valamint a rendszer GAC-ban (Global Assembly Cache). Ezt elkerülendő, a fenti esetben csak az adott alkönyvtárban lévő fileok közül próbál betölteni, mely lehet nem egyezik meg a futó exe alkönyvtárával.

Saját assemblyre hivatkozás

A futó .exe kiterjesztésű file is szerelvénynek minősül a .NET felfogás szerint. Egy olyan szerelvénynek, melynek kiterjesztése nem .dll hanem .exe, és van benne 'Main()' függvény is. Mindez nem mondd ellent az assembly lényegének. Nem szükséges az assemblyket feltétlenül .dll kiterjesztéssel ellátni, és maga a file betöltése a memóriában nem jelenti annak elindítását sem, így lényegtelen szempont, hogy van-e benne 'Main()' függvény vagy sem. Ennek megfelelően tehát az előző alfejezetben említett eljárással nem csak tényleges .dll-ek, hanem akár .exe-k is betölthetőek. Természetesen itt csak .NET kompatibilis dll-ekről, exe-kről van szó. Ezzen a módszerrel nem lehetséges natív (Win32)-es dll-ek, sem .exe-k betöltése.

Maga a futó program is szerelvénynek minősül tehát, csak őt már nem kell betölteni még egyszer a memóriába amennyiben az már betöltődött (fut). A saját assembly-re az alábbi módon kereshetünk referenciát:

using System.Reflection;
 
class PluginManager
{
    public static Assembly myExe()
    {
        return Assembly.GetExecutingAssembly();
    }
}

Mindkét módszerrel, mint látjuk, egységesen egy 'Assembly' típusú objektum-példányhoz juthatunk, mellyel a továbbiakban már egységesen dolgozhatunk.

Egy osztály megkeresése egy szerelvény belsejében

Amennyiben van egy 'Assembly' osztálybeli példányunk, úgy lehetőség van annak belsejében egy tetszőleges osztály megkeresése. Ismernünk kell az adott osztály milyen nevű névtérben helyezkedik el, és ismernünk kell az osztály nevét. A DLL írásakor ismertetett módon ezen osztálynak public láthatóságúnak kell lennie, hiszen csak ezek használhatóak a DLL területén kívül is.

A keresett osztályra referenciát egy 'System.Type' objektumosztálybeli példány fogja hordozni. Ezen 'Type' példányon keresztül további információkhoz juthatunk hozzá az adott, általa képviselt osztályról, például listaszerűen lekérdezhetjük annak milyen metódusai vannak, stb.

Assembyl dll = PluginManager.LoadAssembly("akarmilyenPlugin.dll");
Type t = dll.GetType("enNevterem.enOsztalyom", false, true);

A 'dll' által képviselt assemblyben a 'GetType' függvénnyel lehet keresni. Meg kell adni, milyen névtérbeli milyen osztályt keresünk. A névtér és az osztály neve itt bele van égetve a kódba, de fejletteb esetekben ezt valamilyen konfigurációs állomány is tartalmazhatja akár (pl. xml vagy ini).

A 'GetType' második paramétere, a 'false' azt írja le, hogy amennyiben a fenti nevű osztály nem létezne a dll belsejében, úgy nem kérünk 'Exception'-t, hanem helyette a 'GetType' térjen vissza 'null' értékkel. Amennyiben ez 'true' lenne, úgy a nem létező, nem megtalálható osztály esetén a GetType egy, az okot leíró megfelelő Exception feldobásával reagálna (lásd súgó).

A 'GetType' harmadik paramétere, a 'true' azt jelöli, hogy a keresés kisbetű-nagybetű eltérésre érzéketlen módon hajtódjon végre. A C és a C# szintaktika kisbetű-nagybetű érzékeny egyéb esetekben, de a keresés során ettől eltérhetünk (mint a fenti esetben).

A fenti példában a 't' változóba vagy 'null' értéket kapunk, vagy egy, a keresett, megadott nevű osztályról szóló információt leíró Type példány referenciája kerül.

Egy osztály metódusának megkeresése

Amennyiben rendelkezésünkre áll egy 'Type' példány, úgy azon keresztül az adott osztályról további információkat kérhetünk le. Amennyiben metódusokat keresünk, úgy a 'MethodInfo' osztály példányait kapjuk meg mint keresési eredmény:

MethodInfo creator = t.GetMethod("Create", BindingFlags.Public | BindingFlags.Static);

A fenti kód szerint a 't' példány ('enNevterem.enOsztalyom' osztály) 'Create' nevű metódusát keressük. Ezen metódus a fenti példa szerint legyen 'public' és 'static' jelzőjű, vagyis osztályszintű. A paraméterlistájára most nem írtunk elő semmit.

MethodInfo dosome = t.GetMethod("DoSomething", 
    BindingFlags.Public | BindingFlags.Static, 
    null, 
    new Type[] { typeof(int), typeof(string), typeof(int) }, 
    null);

Ebben a példában már pontosítunk a keresésen: olyan 'DoSomething' függvényt keresünk, amely osztályszintű, publikus, és három paraméterű, sorban 'int', 'string' és 'int' típusokkal.

Teljesen hasonlóan kereshetünk mezőket (MemberInfo példányokat kapunk a .GetMember(...) függvénnyel), konstruktorokat (ConstructorInfo példányokat kapunk a .GetConstructor(...) függvénnyel), de akár property-ket is lekérdezhetünk. Lekérdezhetjük az adott típus milyen interface-ekkel kompatibilis, ki az őse, stb...

Osztályszintű metódus meghívása I.

Amennyiben a fenti 'GetMethod' segítségével egy metódust megtaláltunk, úgy lehetőségünk van azt meghívni:

// a függvény akit meghívunk az alábbi szignatúrájú:
//    public static void Create() { ... }
 
creator.Invoke(null, null);

Az 'Invoke' segítségével hívhatunk meg egy metódust. Az első paramétere ('null') jelentése szerint ez nem példányszintű metódus (nem az, hiszen osztályszintű a 'Create(...)'. A második paramétere szerint (szintén 'null') nem vár paramétert.

Az 'Invoke' egyébként meghívja a megadott függvényt, majd annak visszatérési értékét visszaadná. Ezzel most nem foglalkozunk, hiszen a függvényünk most 'void' visszatérési értékű.

Osztályszintű metódus meghívása II.

Amennyiben a metódus paramétert vár, úgy azt természetesen át kell neki adni:

// a függvény akit meghívunk az alábbi szignatúrájú:
//    public static void DoSomething(int a, string r, int b) { ... }
 
dosome.Invoke(null, new Object[] { 12, "Hello", 20 });

Mint látjuk, az 'Invoke' első paramétere maradt 'null', hiszen továbbra is osztályszintű metódust hívunk. A második paraméterében azonban a kereséskor is specifikált paraméterlistának megfelelően elhelyezünk egy int-et, egy string-et, majd még egy int-et.


Példányszintű konstruktor megkeresése és példányosítás

Amennyiben a betöltött assemblyben szereplő osztályt példányosítani szeretnénk (mivel egy példányszintű metódus meghívása a feladat), úgy első lépésként a konstruktort kell felderíteni. Ehhez egy 'ConstructorInfo' példány megszerzése a cél:


// a példában keresett konstruktor 'konst(int,string)' paraméterezésű
ConstructorInfo c = t.GetConstructor( new Type[] { typeof(int), typeof(string) }  );

Amennyiben sikeresen felderítettünk egy példányszintű metódust, úgy azon keresztül létrehozhatunk példányt:

// az (int,string) paraméterezésű konstruktor hívása
Object x = c.Invoke(new Object[] { 12,"Hello" }) );

Az 'Invoke()' egy általános 'Object' típusú értéket ad vissza, de a generált, 'x' példány valójában egy, az adott osztályból kreált példány. Az 'is' operátorral erről meg is győződhetnénk, de persze ezt most nem használhatjuk, mivel a típusnevet nem használhatjuk fel a kódunkban.


Példányszintű metódus megkeresése és meghívása

Ennek ellenére az 'x' példány adott metódusát meg tudjuk hívni, ehhez típuskényszerítésre sincs szükség. A 'GetMethod()' segítségével felderítjük a szokásos módon a metódust az osztály belsejében. Ekkor természetesen nem 'BindingFlags.Static' jellegű metódust keresünk, hanem 'BindingFlags.Instance' jellegűt (instance = példány):

MethodInfo calc = t.GetMethod("calcDivide", 
    BindingFlags.Public | BindingFlags.Instance, 
    null, 
    new Type[] { typeof(int), typeof(int) }, 
    null);

A példányszintű metódus meghívásához továbbra is az 'Invoke()' használható, de első paramétereként ekkor az előzőekben létrehozott példányt ('x') is meg kell adni. Amennyiben a függvény visszatérési értékkel is rendelkezik - az 'Invoke()' általános 'Object' típusú visszatérési értékét a megfelelő típusra kell kényszeríteni:

// a meghívandó fv 'double'-val tér vissza, és 'fv(int,int)' paraméterezésű
int res = (double) calc.Invoke(x, new Object[] { 12, 20 });
Hernyák Zoltán
A lap eredeti címe: „http://wiki.ektf.hu/wiki/Mp3/ea27
Nézetek
nincs sb_44.201.59.20 cikk