Bevezetés
A reflekció a programok olyan képessége, amellyel futási időben tudja vizsgálni és módosítani a program szerkezetét és viselkedését (általában ez adattagokat, eljárásokat, függvényeket jelent de komplett osztályokra is vonatkozhat).
Aki ahhoz szokott, hogy ilyeneket nem lehet megtenni, annak elsőre furcsának tűnhet és egy kicsit valóban az. Érdekes a koncepció és néhány dologra igen hasznos, de ezzel együtt veszélyeket rejt. Az ebben járatos szakemberek is javasolják a bonyolult reflekció elkerülését.
Képzeljük el egy pillanatra, hogy egy hatalmas programon dolgozunk és valaki előttünk egy olyan funkciót implementált, amelyben valamilyen reflekciós műveletek vannak. Ezt nem feltétlenül tudjuk előre és mikor oda kerülünk, hogy esetleg ezt a részt javítani kéne, a debugger nem igazán (vagy egyáltalán nem) ad visszajelzést, így nagyon sok időt eltölthetünk azzal, mire kibogarásszuk a részleteket.
Tehát az első szempont ami alapján mérlegelni kell, hogy mennyire van szükség reflectionre a felhasználhatóság és olvashatóság, barátságosság.
De természetesen vannak esetek, mikor egy szép megoldáshoz feltétlenül szükség lehet néhány műveletre.
A legtöbb, ha nem minden futásidejű reflekciós támogatás az NSObject (ős)osztálytól származik (mint például a Java Object osztálya). Tehát minden ilyen típusú osztálynak van hozzáférése a reflekcióhoz.
Rövid összehasonlítás
Alapvetően ugyan arra az ötletre épül mindegyik és az elméletet tekintve nem is sokban különböznek, de fontos figyelembe venni, hogy melyik-mennyire könnyen használható egy-egy probléma megoldásához. Az alap elvárásokat mint a típusellenőrzés, lekérdezések minden reflekciót támogató nyelv tudja.
Java esetén például bytekódban kell módosításokat végeznünk ahhoz, hogy futási időben tudjunk osztályokat létrehozni, ami igen bonyolult és időigényes dolog.
C# esetén pedig a köztes nyelvben (IL) kell módosításokat végezni ami hasonlóan a Java-hoz, nehéz művelet.
C++-ban pedig még csak évek múlva tervezik bevezetni a fordítási idejű reflectiont, de már folynak róla az egyeztetések.
Így az Objective-C reflekciós adottságait figyelembe véve, egy kicsit talán könnyebben kezelhető, mint a más, népszerű nyelvek, de legalábbis többet megenged számunkra.
A reflection felhasználási lehetőségei
Elsősorban tesztelésnél vehetjük hasznát. Például ha egy típusellenőrzést szeretnénk végezni, akkor így könnyen, egy elágazással megtehetjük, hogy lekérjük az adott osztály adatait (vagy csak a típusát) és azt összehasonlítjuk a kívánt típussal.
Annak lekérdezése, hogy az osztály milyen típusú:
Class class = [MyClass class];
Annak lekérdezése, hogy egy objektum milyen típusú:
Class class = myObject.class;
Remélhetőleg világos, hogy ennek miért van szerepe. Egy dinamikus programnál, ahol a típus futási időben dől el, könnyen változhatnak az igények, ezért fontos tudni meghatározni, hogy az adott pillanatban milyen típust képvisel a példány.
Annak vizsgálata, hogy egy objektum egy adott típushoz tartozik-e:
if ( [anObject
isMemberOfClass:someClass] )
...
Annak vizsgálata, hogy egy objektum egy adott típushoz, vagy annak leszármazottjához tartozik-e:
if ( [anObject
isKindOfClass:someClass] )
...
Annak vizsgálata, hogy egy objektum képes-e egy adott selectorra válaszolni:
SEL s = @selector(initWithEHA:);
if ( [gabor respondsToSelector:s])
[gabor performSelector:s];
Annak vizsgálata, hogy egy osztály megvalósít-e egy adott interface-t (Objective-C-ben protocol néven találjuk):
[myDummyObject conformsToProtocol:@protocol(MyDummyInterface)];
Ezekkel a technikákkal és az ezen oldalon is ajánlott tesztelési eljárásokkal vegyítve egész könnyedén tesztelhetünk bonyolult műveleteket, olyan környezetben ahol egyébként nem biztos, hogy ezt megtehetnénk. Például ha egy osztály privát adattagjait vagy metódusait szeretnénk elérni, nyilván célszerű gettereket/settereket vagy erre kijelölt publikus dolgokat használni, de lehet olyan eset, mikor ez körülményes lenne vagy “túl sokat” adnánk ki ilyenkor. De ilyen módon, lekérhetjük a privát adattagokat és azokat is vizsgálhatjuk.
Lekérhetjük egy adott osztály metódusait:
unsigned int numberOfMethods;
//get the method list of class as a list
Method* methods = class_copyMethodList([ClassName class], &numberOfMethods);
//print the methods from the list
NSLog(@"The class has: %d method(s)", numberOfMethods);
for (int i = 0; i < numberOfMethods; i++)
NSLog(@"Method name: %@", NSStringFromSelector(method_getName(methods[i])));
Megjegyzés
Ezeket a metódusokat akár “selector”-ként arra is felhasználhatjuk, hogy "kiváltsuk" őket. Tehát ha van egy előre struktúrált osztályunk, amelyben a metódusok sorrendben vannak, akár arra is van lehetőség, hogy az összes metódust automatikusan kiváltsuk, anélkül, hogy tudnánk a nevüket.
Másodsorban pedig például komplett osztályokat készíthetünk dinamikusan. A komplett osztály alatt ide értem az adattagokat, műveleteket és a megvalósítást. Vagy pedig belenyúlhatunk már meglévő osztályok viselkedésébe, ezzel akár teljesen megváltoztatva azt.
Most egy pillanatra gondoljunk vissza az előző, debugolhatóságos bekezdésre és már talán érthető a példa.
A futás többnyire dinamikus, ezért lehet megtenni azt, hogy futási időben definiálunk olyan metódusokat amelyek egészen addig nem is léteztek a headerben és implementáljuk őket vagy kiegészítünk már meglévő osztályokat valamivel.
A példáról bővebben
A példában arra törekedtem, hogy sok lehetőséget be tudjak mutatni, így előkerülnek valószínűleg olyan dolgok is, amelyeket a valós életben célszerű (sőt, kötelező) elkerülni.
Az alap szituáció, hogy létre szeretnénk hozni egy SuperClass-t, majd ennek adni egy műveletet amelyet fel tudunk használni.Tehát ehhez az osztályhoz nem tartozik sem interface sem implementation, így nem lehet hivatkozni rá, mint egy "sima" osztályra, míg nincs megkonstruálva.
A következő lépés, hogy kell egy SubClass. Mivel itt is érvényesek az alapvető OOP elvek, ezért a SubClass meg fogja örökölni az ősosztály műveletét és ezáltal fel tudjuk használni ebben is. Ez azt az eredményt fogja adni, hogy az ősosztályban megadott implementációval fog lefutni a művelet.
De, ha felül szeretnénk definiálni az örökölt műveletet, azt már kicsit nehezebben tudjuk megtenni. Ennél több módszer van, de ezek közül csak egyet láthatunk.
Az egyik megoldás, hogy miután létrehoztuk az osztályt, az adott művelet implementációját kicseréljük egy sajátra egy úgynevezett exchange-method művelettel. A második megoldás (amit itt is használunk), hogy már az osztály létrehozásakor megmondjuk, hogy nekünk nem az az implementáció kell és a saját, ugyan olyan szignatúrájú műveletünket tesszük bele.
Ezt akkor tehetjük meg, mikor már “regisztráltuk” az új osztályt a rendszerbe, így az megkeresi az adott műveletet és lecseréli a saját változatra.
Új osztály és példány létrehozásához néhány dolgot kell csak tenni.
Új runtime osztály és példány létrehozása:
//create a class
Class runtimeClass = objc_allocateClassPair([runtimeSuperClass class], "RuntimeClassName", 0);
//register the class
objc_registerClassPair(runtimeClass);
//create an instance of the subclass
id instanceOfRuntimeClass = [[runtimeClass alloc] init];
Az "objc_allocateClassPair" művelet első paramétere az ősosztály típusa, második pedig az új osztály neve.
Ha eltekintünk attól a ténytől, hogy gyakorlatilag lehetetlen lenne nyomon követni sok ehhez hasonló műveletet, ez a kód könnyen olvasható és érthető.
Új művelet hozzáadására két lehetőségünk van. Az első, amely sokkal bonyolultabb és olvashatatlanabb az, hogy saját encode-olt szignatúrát hozunk létre.
Új művelet hozzáadása az osztályhoz, "hard-encode"-dal:
//add the type by getting the info from another class
Method description = class_getInstanceMethod([NSObject class], @selector(description));
NSString *typesNS = [NSString stringWithFormat: @"%s%s%s%s",
@encode(NSUInteger),
@encode(id), @encode(SEL),
@encode(id)];
const char *types = [typesNS UTF8String];
//'override' the 'description' method with the other implementation
class_addMethod(runtimeSubClass, @selector(description), (IMP)SubMethod, types);
Ám ehhez a megoldáshoz is azt a trükköt kell alkalmazni, hogy az NSObject “description” metódusát elkérjük, ezzel megkönnyítve a saját dolgunkat.
Opcionális lépés, hogy ha tehetjük, hozzunk létre egy saját osztály, egy olyan művelettel, amelynek szignatúrája megegyezik az általunk kívánttal és ezt el tudjuk kérni.
Új művelet hozzáadása az osztályhoz, "easy-encode"-dal:
//add the type by getting the info from another class
Method description = class_getInstanceMethod([NSObject class], @selector(description));
const char *types = method_getTypeEncoding(description);
//now add the method (with signature and implementation)
class_addMethod(runtimeSuperClass, @selector(description), (IMP)MainMethod, types);
Ezzel a módszerrel megkímélhetjük magunkat, egyrészt a keresgéléstől, másrészt attól, hogy érthetetlen szignatúra encode stringeket kellene kitalálnunk.
Az izgalmasabb része viszont az, hogy lehetőségünk van a subclassból kiváltani a superclass már overrideolt műveletét.
Ehhez az objc/runtime ad segítséget, ahol megmondhatjuk, hogy mi is a mi superclassunk és egy objc_super típusú objektumban megadva ezen információkat már tudunk üzenetet küldeni az osztálynak, hogy mely műveletét szeretnénk meghívni.
az eredeti metódus meghívása a gyermekosztályból:
//create a superclass object from the subclass
struct objc_super superClass = {
.receiver = instanceOfRuntimeSubClass,
.super_class = class_getSuperclass([instanceOfRuntimeSubClass class])
};
//call the superclass's method from subclass
NSLog(@"2. call SuperClass's method, from SubClass.");
NSLog(objc_msgSendSuper(&superClass, @selector(description)));
Property hozzáadása
Ha már hozzáadtuk a változónkat az osztályhoz, írhatunk hozzá property-t is, amely a lekérdezést és a beállítást segíti.
Property hozzáadása az osztályhoz:
//create a new class with default _privateName attribute
@interface Dummy : NSObject {
NSString* _privateName;
}
@end
//in the 'Init' method, set the _privateName attribute to "Any"
implementation Dummy
– (id)init {
self = [super init];
if (self) _privateName = @”Any”;
return self;
}
@end
//create a getter method for the property, we will get its implementation
NSString *nameGetter(if self, SEL _cmd) {
Ivar ivar = class_getInstanceVariable([Dummy class], “_privateName”);
return object_getIvar(self, ivar);
}
//create a setter method for the property, we will get its implementation
void nameSetter(if self, SEL _cmd, NSString* newName) {
Ivar ivar = class_getInstanceVariable([Dummy class], “_privateName”);
id oldName = object_getIvar(self, ivar);
if (oldName != newName) object_setIvar(self, ivar, [newName copy]);
}
int main(void) {
@autoreleasepool {
objc_property_attribute_t type = { “T”, “@\”NSString\”” }; //Type : NSString
objc_property_attribute_t ownership = {“C”, “”}; //C - Copy
objc_property_attribute_t backingivar = {“V”, “_privateName”}; //Variable : _privateName
objc_property_attribute_t attrs[] = { type, ownership, backingivar }; //array of property attributes
class_addProperty([SomeClass class], "name", attrs, 3); //type, property name, attributes and count
class_addMethod([SomeClass class], @selector(name), (IMP)nameGetter, "@@:");
class_addMethod([SomeClass class], @selector(setName:), (IMP)nameSetter, "v@:@");
id obj = [Dummy new]; //create a new instance
NSLog(@”%@”, [obj name]); //get _privateName through name property
[obj setName: @”Name”]; //set the _privateName through name property
NSLog(@”%@”, [obj name]); //get the new value of the variable
}
}
A main-ben látható, hogy mit csinál valójában a program. Először is, meg kell konstruálnunk a propery-t, amelyet használni szeretnénk. Ehhez már az előre megírt getter és setter metódust használjuk fel. A class_addProperty hozzáadja a property-t a osztályhoz, majd a class_addMethod hozzáadja a megadott névvel a metódusokat.
Ennek eredményeképpen az “Any” és “Name” szövegeket kapjuk eredményül a kiiratásoknál, mivel alapértelmezetten az "Any" string került bele a "name" attribútumba, majd pedig a "setName" művelet miatt egy "Name".
Felvetődhet a kérdés, hogy ha új osztályt képesek vagyunk futási időben létrehozni, akkor vajon tudunk-e törölni is.
A válasz sajnos az, hogy nem. Még egy időben volt erre lehetőség, de ezt később elvetették. Nem is csoda.
Összegzés
Alapvetően az Objective-C reflekciós megvalósítása hasznos és jól struktúrált. Olvashatóságát sikerült megtartania és könnyen (de minimum könnyebben, mint más nyelvekben) kezelhető. Azonban sosem feledkezzünk meg arról a tényről, hogy ha ilyen módszereket vagyunk kénytelenek alkalmazni, akkor valami nincs rendben a szoftverünk felépítésével.
Reflection az Apple Developers-től
A https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html oldalon rengeteg hasznos információt kaphatunk a további felhasználási lehetőségekről. Minden elérhető művelet leírását megtalálhatjuk, hozzá némi magyarázattal.