Reflection-alapú szám parsolás

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.