databájt 2010.12.01. 13:46

All your properties are belong to us

 Jó napot, jó kódolást! 

Bizonyára járt már minden fejlesztő abban a cipőben, hogy adott problémára valamilyen általános, „Reflection-alapú” megoldásra volt szüksége. Azoknak ez az írás talán felbecsülhetetlen információt jelent a .NET egy újabb turpisságával kapcsolatban.

De ne szaladjunk előre, ejtsünk pár szót arról, mi is az a reflection, és hogy mire használható! 

Definíció, as per MSDN:

„Reflection provides objects that encapsulate assemblies, modules, and types.

You can use reflection to dynamically create an instance of a type, bind the type to an existing object, or get the type from an existing object.

You can then invoke the type's methods or access its fields and properties.” 

Vagyis különböző, egy adott típusra jellemző metódusokat, property-ket használhatunk úgy, hogy nem egy konkrét típusra írunk kódot.

A property-k leírója a reflection-ben PropertyInfo, aminek segítségével beállíthatunk (SetValue) vagy lekérdezhetünk (GetValue) egy értéket, valahogy így: 


User user = new User();
PropertyInfo property = typeof (User).GetProperty("Name");
string name = (string) property.GetValue(user, null); // name = user.Name
property.SetValue(user, "Admin", null); // user.Name = "Admin"
 


 Ez nyilván egy olyan helyen, mint pl. az Invenio adatelérési rétege, lehetővé teszi számunkra, hogy egy adott feladatra általános megoldást adjunk. Például, amikor az adatbázisból olvasunk ki adatokat, az adatbázisrekordok objektumokká alakításánál rendre kiolvasunk egy értéket a rekordból, majd azt egy .NET objektumon, mint property értéket beállítunk. Az Invenio minden típusról nyilvántart egy TypeContainer példányt, ami tartalmazza a típusra jellemző tulajdonságokat, attribútumokat, property-ket, utóbbiakat MemberContainer példányokban. 

Ezek segítségével így olvasunk ki a rekordokból: 


internal class EntityReader : IEntityReader
{
       private readonly TypeContainer _container = null;
       private List<RecordBinder> _binders = null;
                ... 

public object ReadItem(IDataRecord dataRecord, CultureInfo currentCulture)
{
       using (NDC.Push("ReadItem"))
       {
             object instance = _container.Instantiate(); // pl. adott esetben User u = new User()
             int binderCount = _binders.Count; // vö. bindelhető property-k száma
             for (int i = 0; i < binderCount; i++)
             {
                    RecordBinder binder = _binders[i]; // Egy adott property-hez tartozó binder
                    if (!binder.IsBoundToNull(dataRecord))
                    {
                           binder.Bind(dataRecord, instance);
                           if (binder is MultilingualStringRecordBinder)
                           {
                                 ((MultilingualString)binder.Member.GetValue(instance)).DefaultCulture = currentCulture;
                           }
                    }
             }
             return instance;
       }
}
...

}


 Ez az egyetlen kód felel az összes típus kezeléséért, legyen az Content, Segment, User, stb. A konkrét property olvasást a RecordBinder absztrakt class különböző implementációi végzik, a leggyakrabban használt a SimpleRecordBinder ami pl. DateTime, string, stb. típusokat kezel. Íme egy részlet a SimpleRecordBinder kódjából: 


/// <summary>
/// Data record binder class.
/// </summary>
private class SimpleRecordBinder : RecordBinder
{
       ...
       /// <inheritdoc/>
       public override bool IsBoundToNull(IDataRecord dataRecord)
       {
             return dataRecord.IsDBNull(_ordinal);
       }
       /// <inheritdoc/>
       public override void Bind(IDataRecord dataRecord, object instance)
       {
             Member.SetValue(instance, dataRecord.GetValue(_ordinal));
       }
       ...
}
 


 

A metódusban a Member property az absztrakt ősben található MemberContainer példány, aminek a SetValue metódusa leegyszerűsítve valahogy így néz ki:


 

...
public void SetValue(object instance, object value)
{
       if (null == value)
             return; 

       object preparedValue = value.GetType() == _actualType ? value : PrepareValue(_actualType, value);
       ((PropertyInfo)_innerInfo).SetValue(instance, preparedValue, null);
}
...


 

Ezzel el is érkeztünk a lényegi részhez. Történt ugyanis, hogy napi performancia nézegetésem közben rápillantottam a fenti sor  futási idejére, és megdöbbentem:

 



 

Mit látnak szemeink? Egy egyszerű értékadás (429-es sor), ami nagyságrendileg 800-1000 tick környékén szokott tartani, itt durván 38.000. HarmincnyolcEZER!

Na, itt kinyílt a zsebemben a refaktor gomb aranyveretes doboza, és utánaszaglásztam kicsit. 

Háttal nem kezdünk mondatot, de mivel a mondat előttem van... hát a reflector szerint ez történik a PropertyInfo.SetValue metódus bugyraiban:



 

A sorok közül az időrablásért felelős  a setMethod.Invoke kezdetű.

-          Egy nagyon egyszerű értékadásról beszélünk, miért várunk rá ennyit?

-          Mert írva vagyon, azért! 

És mi lenne, ha átírnánk a játékszabályokat, és megoldanánk okosba’, hogy a PropertyInfo.SetValue hókuszpókusza helyett közvetlenül a kívánt műveletet, és csak azt hajsuk végre?

Éppen erre használható a System.Reflection.Emit namespace-ben található DynamicMethod. 

Nosza legózzunk egyet, lássuk, hogyan lehet futási időben építeni egy olyan setter metódust, ami paraméterként egy objektumpéldányt és a beállítandó értéket várja, majd elvégzi a property = value műveletet!


...
Type[] arguments = new Type[2];
arguments[0] = arguments[1] = typeof(object);
System.Reflection.Emit.DynamicMethod setter =
       new System.Reflection.Emit.DynamicMethod("_Set" + propertyInfo.Name + "_", typeof(void), arguments, propertyInfo.DeclaringType);
ILGenerator generator = setter.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
generator.Emit(OpCodes.Ldarg_1); 

if (propertyInfo.PropertyType.IsClass)
       generator.Emit(OpCodes.Castclass, propertyInfo.PropertyType);
else
       generator.Emit(OpCodes.Unbox_Any, propertyInfo.PropertyType);
generator.EmitCall(OpCodes.Callvirt, setMethod, null);
generator.Emit(OpCodes.Ret); 

Action<object, object> setterMethodDelegate = (Action<object, object>)setter.CreateDelegate(typeof(Action<object, object>));
...


 

A fenti módszert használva úgy módosítottam a MemberContainer.SetValue metódust, hogy a röptében készített setter metódust használja a hagyományos, reflection-ben található PropertyInfo.SetValue helyett.

Alább látható a megismételt mérés eredménye.

 



 

Bizony, a 440-es sort összehasonlítva a korábbival 38.250 helyett immár csak 1.325 tick volt a property érték beállítása. A mérések során az utóbbi érték valahol 1000 és 1400 környékén mozgott, ez nyilván a gép egyéb háttérben futó folyamataitól is függ.

Normál esetben azért azt mondhatjuk, hogy durván 20-30-szoros a különbség a DynamicMethod javára. Ugyanez a tendencia egyébként a GetValue esetében is megfigyelhető, ezt is érdemes DynamicMethod-al kiváltani. 

Tanulság 

Röviden összefoglalva:

·         A reflection jó lehetőségeket kínál a típusfüggő műveletek általános megoldására (1 egyszerű, általános eljárás a soktíz-sokszáz gondosan felépített típusfüggő megoldás helyett)

·         A PropertyInfo SetValue() és GetValue() metódusai (vélhetően a FieldInfo ezen metódusai is) látványosan lomhák, különösen érdemes helyettük dinamikusan getter/setter metódusokat építeni ha számít a sebesség

·         A menzás ebéd még mindig drága, de legalább jó a körömpörköltjük

Szólj hozzá!

A bejegyzés trackback címe:

https://dotnot.blog.hu/api/trackback/id/tr714295604

Kommentek:

A hozzászólások a vonatkozó jogszabályok  értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai  üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a  Felhasználási feltételekben és az adatvédelmi tájékoztatóban.

Nincsenek hozzászólások.
süti beállítások módosítása