A reflection, azaz az objektumorientált nyelvekben a különböző osztályok metaadataihoz hozzáférés, akármennyire szeretjük vagy nem, megkerülhetetlen. Nincs olyan modern felhasználói felületi keretrendszer, ami ne használná nagyon erőteljesen, erre az egyik legelterjedtebb példa a WPF, aminek a legalapvetőbb funkciói reflekcióra épülnek,
Természetesen jobb kerülni a felesleges reflection használatot, ahol lehet interfészek és egyéb „konzervatív” megoldások általánosságban előnyben vannak részesítve. Vannak ugyanakkor esetek amikor minden más elbukik. .NET környezetben egy ilyen terület az elemi számszerű típusok – intek, shortok, bíteok, floatok, stb. Ezek, hasonlóságukból adódóan, akár egy közös ősből is származhatnának (pl. MaxValue
propertyje van az összes egészt tároló típusnak, NaN
propertyje a lebegőpontos típusoknak stb.), ez – szintén érthető módon – mégsincs így.
A fentebb említett tulajdonságok miatt lehet érdekes a probléma, amibe belefutottunk. Adott egy PropertyInfo
objektum, és egy mögöttes object
. Az objektumról pontosan tudtuk, hogy számszerű primitív, így kell legyen Parse
és TryParse
metódusa is. Sajnos nem egy közös ősből, egyszerűen úgy alakult, hogy mindegyik számszerű típus külön, ugyanolyan paraméterezéssel definiálja ezeket a metódusokat. Meghívni így egy ilyen TryParse
metódust az objektumra vagy erőltetett és ronda switch-case-eken keresztül ugyan történhetett volna azonban annál egy sokkal elegánsabb, reflection-re épülő megoldást sikerült találnunk.
Első körben a biztonság kedvéért rögzítettük a kezelt típusok halmazát:
public List<Type> ParseableTypes = new List<Type>() { typeof(int), typeof(uint), typeof(short), typeof(ushort), typeof(long), typeof(ulong), typeof(decimal), typeof(float), typeof(double), typeof(byte) };
Majd egy gyors vizsgálat után meg lehet hívni a TryParse
metódust.
if(this.ParseableTypes.Contains(this.BoundProperty.PropertyType)) { MethodInfo parser = this.BoundProperty.PropertyType.GetMethod( nameof(int.TryParse), new[] { typeof(string), this.BoundProperty.PropertyType.MakeByRefType() } ); object[] parameters = new object[] { e.Value, null }; bool result = (bool)parser.Invoke(null, parameters); if (result) { this.BoundProperty.SetValue(this.BoundItem, parameters[1]); } }
Tömör, de nem triviális kód, nézzük lépésről lépésre.
Bejövő objektum típusvizsgálata
if(this.ParseableTypes.Contains(this.BoundProperty.PropertyType))
A feltételt talán nem kell részletesen magyarázni, amennyiben benne van a PropertyInfo
által mutatott típus a kezelt típusok között, végrehajthatjuk a kódot. Leszármazott típusok nem lehetnek, az összes felsorolt kezelt típus struct
, így nem származtatható.
GetMethod
és használata
MethodInfo parser = this.BoundProperty.PropertyType.GetMethod( nameof(int.TryParse), new[] { typeof(string), this.BoundProperty.PropertyType.MakeByRefType() } );
A GetMethod
metódus egy MethodInfo
objektummal tér vissza. Ennek Invoke
metódusát meghívva végrehajtható egy adott objektumra az objektum metódusa, amit a MethodInfo
leír. Első paramétere a lekérendő metódus neve (mi itt egy nameof
szerkezetet használtunk, de ez egyenértékű a „TryParse” stringgel, viszont így nem magic constant). A Második paraméter egy Type
tömb, a metódus paramétereinek típusai. Ezekkel az információkkal (név és paraméterek típusai) gyakorlatilag a metódus szignatúráját adjuk át.
TryParse
és az out
paraméter
Szemfüles és C#-ban jártas olvasók már észrevehették, hogy a TryParse
-ok definíciója a következőhöz hasonló:
public struct Int32 { ... public static bool TryParse(string s, out Int32 result); .... }
A második paraméter egy out
kulcsszóval van megjelölve, tehát a bemenetet – annak ellenére, hogy jelen esetben mindig értékalapú lesz – referenciaként adjuk át, amibe a metódus siker esetén a parsolt számszerű értéket fogja adni.
Ez az out int
egy külön, referenciaként kezelt int típus, az hogy egy extra kulcsszóval van leírva csak nyelvi trükközés. Ezért ha a GetMethod
második paraméterében egy sima typeof(int)
-et adnánk át a visszatért érték null lenne, hiszen ilyen szignatúrájú metódus nem létezik.
Ezt az ellentétet tudjuk a MakeByRefType
metódus által visszaadott típussal feloldani, amely bármilyen átadott típusból (legyen ez pl. T
) egy ref T
típust hoz létre. Ezt a visszaadott Type
-ot már aztán használhatjuk a paramétertípusok tömbjében és sikeresen visszakapjuk a parsoló metódus leíróját.
A metódus meghívása és eredmény kinyerése
object[] parameters = new object[] { e.Value, null }; bool result = (bool)parser.Invoke(null, parameters);
A metódus meghívása a már említett out
kulcsszavas paraméter miatt még figyelmet érdemel. A MethodInfo.Invoke
meghívásakor egy példányt és egy paramétertömböt kell átadnunk. A példánynak értelemszerűen annak az objektumnak kell lennie, amelynek a leírt metódusát akarjuk meghívni. Tekintettel, hogy a TryParse
egy statikus metódus, ezért ez null
. A paraméterlistában az out int
„helyére” szintén egy null értéket adunk meg. Ebben ugyanakkor nincs ellentmondás, a null egy érvényes referencia-alapú érték! A lényeg, hogy legyen hozzá (mármint a null-t értékül kapó változóhoz) referenciánk, hiszen ide fog beíródni a TryParse
eredménye. Mivel tömbben adjuk át a paramétereket, ezért a referenciánk egyértelmű, indexeléssel elérhető.
Értékvisszírás
if (result) { this.BoundProperty.SetValue(this.BoundItem, parameters[1]); }
Az utolsó lépés a PropertyInfo.SetValue
meghívása. Itt az egyenetlen igazán érdekes, hogy egy ismeretlen típusú, objektumot adunk át. Ez persze pontosan egy olyan típusú objektum, amire nekünk szükségünk van, ezt TryParse
biztosítja, de a reflection sajátossága, hogy a legtöbb kezelt adatot object
-be dobozolva használunk. Vizsgálni kéne hogy valóban helyes típust akarunk-e odaadni de ezt gyakorlatilag a folyamat előző lépései biztosítják.