Captador / definidor de campos con árbol de expresiones en la clase base

c# expression-trees fieldinfo reflection setter

Pregunta

Siguiendo los ejemplos de esta publicación y su pregunta de seguimiento , estoy intentando crear captadores / definidores de campo utilizando expresiones compiladas.

El captador funciona simplemente bien, pero estoy bloqueado, ya que necesito que el asignador asigne cualquier tipo de campos.

Aquí mi creador de la acción del instalador:

public static Action<T1, T2> GetFieldSetter<T1, T2>(this FieldInfo fieldInfo) {
  if (typeof(T1) != fieldInfo.DeclaringType && !typeof(T1).IsSubclassOf(fieldInfo.DeclaringType)) {
    throw new ArgumentException();
  }
  ParameterExpression targetExp = Expression.Parameter(typeof(T1), "target");
  ParameterExpression valueExp = Expression.Parameter(typeof(T2), "value");
  //
  // Expression.Property can be used here as well
  MemberExpression fieldExp = Expression.Field(targetExp, fieldInfo);
  BinaryExpression assignExp = Expression.Assign(fieldExp, valueExp);
  //
  return Expression.Lambda<Action<T1, T2>> (assignExp, targetExp, valueExp).Compile();
}

Ahora, guardo los configuradores genéricos en una lista de caché (porque, por supuesto, construir el configurador cada vez es un asesino de rendimiento), donde los emito como simples "objetos":

 // initialization of the setters dictionary
 Dictionary<string, object> setters = new Dictionary(string, object)();
 Dictionary<string, FieldInfo> fldInfos = new Dictionary(string, FieldInfo)();
 FieldInfo f = this.GetType().GetField("my_int_field");
 setters.Add(f.Name, GetFieldSetter<object, int>(f); 
 fldInfos.Add(f.Name, f); 
 //
 f = this.GetType().GetField("my_string_field");
 setters.Add(f.Name, GetFieldSetter<object, string>(f); 
 fldInfos.Add(f.Name, f); 

Ahora trato de establecer un valor de campo como este:

 void setFieldValue(string fieldName, object value) {
      var setterAction = setters[fieldName];
      // TODO: now the problem => how do I invoke "setterAction" with 
      // object and fldInfos[fieldName] as parameters...?
 }

Simplemente podría llamar a un método genérico y lanzar cada vez, pero me preocupa la sobrecarga de rendimiento ... ¿Alguna sugerencia?

- RESPUESTA EDITADA Basado en la respuesta del Sr. Anderson , creé un pequeño programa de prueba que compara directamente la configuración del valor, la reflexión en caché (donde se almacenan en caché los FieldInfo) y el código multitipo en caché. Uso la herencia de objetos con hasta 3 niveles de herencia ( ObjectC : ObjectB : ObjectA ).

El código completo del ejemplo se puede encontrar aquí.

Una sola iteración de la prueba da el siguiente resultado:

-------------------------
---      OBJECT A     ---
-------------------------
  Set direct:       0.0036 ms
  Set reflection:   2.319 ms
  Set ref.Emit:     1.8186 ms
  Set Accessor:     4.3622 ms

-------------------------
---      OBJECT B     ---
-------------------------
  Set direct:       0.0004 ms
  Set reflection:   0.1179 ms
  Set ref.Emit:     1.2197 ms
  Set Accessor:     2.8819 ms

-------------------------
---      OBJECT C     ---
-------------------------
  Set direct:       0.0024 ms
  Set reflection:   0.1106 ms
  Set ref.Emit:     1.1577 ms
  Set Accessor:     2.9451 ms

Por supuesto, esto simplemente muestra el costo de crear los objetos, lo que nos permite medir el desplazamiento de la creación de las versiones en caché de Reflection and Expressions.

A continuación, vamos a ejecutar 1.000.000 veces:

-------------------------
---      OBJECT A     ---
-------------------------
  Set direct:       33.2744 ms
  Set reflection:   1259.9551 ms
  Set ref.Emit:     531.0168 ms
  Set Accessor:     505.5682 ms

-------------------------
---      OBJECT B     ---
-------------------------
  Set direct:       38.7921 ms
  Set reflection:   2584.2972 ms
  Set ref.Emit:     971.773 ms
  Set Accessor:     901.7656 ms

-------------------------
---      OBJECT C     ---
-------------------------
  Set direct:       40.3942 ms
  Set reflection:   3796.3436 ms
  Set ref.Emit:     1510.1819 ms
  Set Accessor:     1469.4459 ms

Para completar la información: eliminé la llamada al método "set" para resaltar el costo de obtener el setter ( FieldInfo para el método de reflexión, Action<object, object> para el caso de la expresión). Aquí los resultados:

-------------------------
---      OBJECT A     ---
-------------------------
  Set direct:       3.6849 ms
  Set reflection:   44.5447 ms
  Set ref.Emit:     47.1925 ms
  Set Accessor:     49.2954 ms


-------------------------
---      OBJECT B     ---
-------------------------
  Set direct:       4.1016 ms
  Set reflection:   76.6444 ms
  Set ref.Emit:     79.4697 ms
  Set Accessor:     83.3695 ms

-------------------------
---      OBJECT C     ---
-------------------------
  Set direct:       4.2907 ms
  Set reflection:   128.5679 ms
  Set ref.Emit:     126.6639 ms
  Set Accessor:     132.5919 ms

NOTA: el aumento de tiempo aquí no se debe al hecho de que los tiempos de acceso son más lentos para los diccionarios más grandes (ya que tienen O(1) tiempos de acceso), sino al hecho de que la cantidad de veces que accedemos aumenta (4 veces por iteración para ObjectA , 8 para ObjectB , 12 para ObjectC ) ... Como se ve, solo la compensación de creación hace una diferencia aquí (lo que se espera).

Conclusión: mejoramos el rendimiento en un factor de 2 o más, pero aún estamos lejos del rendimiento del conjunto de campo directo ... Recuperar el configurador correcto en la lista representa un buen 10% del tiempo.

Trataré con árboles de expresión en lugar de Reflexión. Revisa si podemos reducir aún más la brecha ... Cualquier comentario es más que bienvenido.

EDIT 2 Agregué resultados utilizando el enfoque utilizando una clase genérica de "Accessor" como lo sugirió Eli Arbel en esta publicación .

Respuesta aceptada

Si desea que esto admita operaciones en múltiples tipos, su caché de funciones debe ser indexado por el Type y el nombre del campo ( string ), y las funciones deben ser creadas perezosamente. Prueba esto:

private static Dictionary<Type, Dictionary<string, Action<object, object>>> _typeMapper = new Dictionary<Type, Dictionary<string, Action<object, object>>>();

public static void Set(object obj, string fieldName, object newValue)
{
    if (obj == null)
    {
        throw new ArgumentNullException("obj");
    }
    Type type = obj.GetType();
    Dictionary<string, Action<object, object>> fieldMapper;
    Action<object, object> action;
    if (_typeMapper.TryGetValue(type, out fieldMapper))
    {
        // entry has been created for this type.
        if (!fieldMapper.TryGetValue(fieldName, out action))
        {
            // method has not been created yet, must build it.
            FieldInfo fld = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            if (fld == null)
            {
                throw new ArgumentException("No field " + fieldName);
            }
            action = buildSetter(fld);
            fieldMapper.Add(fieldName, action); // add it to method cache for future use.
        }
    }
    else
    {
        // -- ADDED CODE: forgot to create the new fieldMapper.....
        fieldMapper = new Dictionary<string, Action<object, object>>();

     // type has not been added yet, so we know method has not been built yet either.
        FieldInfo fld = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        if (fld == null)
        {
            throw new ArgumentException("No field " + fieldName);
        }
        action = buildSetter(fld);
        fieldMapper.Add(fieldName, action); // add it to method cache for future use.
        _typeMapper.Add(type, fieldMapper); // add it to type cache for future use.
    }
    action(obj, newValue); // invoke the method.
}
// this is my preferred setter-builder, feel free to use expressions instead.
private static Action<object, object> buildSetter(FieldInfo fld)
{
    DynamicMethod dyn = new DynamicMethod("set_" + fld, typeof(void), new[] { typeof(object), typeof(object) }, fld.DeclaringType);
    ILGenerator gen = dyn.GetILGenerator();
    gen.Emit(OpCodes.Ldarg_0);
    gen.Emit(OpCodes.Castclass, fld.DeclaringType);
    gen.Emit(OpCodes.Ldarg_1);
    if (fld.FieldType.IsClass)
    {
        gen.Emit(OpCodes.Castclass, fld.FieldType);
    }
    else
    {
        gen.Emit(OpCodes.Unbox_Any, fld.FieldType);
    }
    gen.Emit(OpCodes.Stfld, fld);
    gen.Emit(OpCodes.Ret);
    return (Action<object, object>)dyn.CreateDelegate(typeof(Action<object, object>));
}

De lo contrario, si solo necesita hacer esto con un tipo, su proceso se convierte en:

private static Dictionary<string, Action<MyType, object>> _mapper = new Dictionary<string, Action<MyType, object>>();

public static void Set(MyType obj, string fieldName, object newValue)
{
    if (obj == null)
    {
        throw new ArgumentNullException("obj");
    }
    Action<MyType, object> action;
    if (!_mapper.TryGetValue(fieldName, out action))
    {
        FieldInfo fld = typeof(MyType).GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        if (fld == null)
        {
            throw new ArgumentException("No field " + fieldName);
        }
        action = buildSetter(fld);
        _mapper.Add(fieldName, action);
    }
    action(obj, newValue); // invoke the method.
}

private static Action<MyType, object> buildSetter(FieldInfo fld)
{
    DynamicMethod dyn = new DynamicMethod("set_" + fld, typeof(void), new[] { typeof(MyType), typeof(object) }, typeof(MyType));
    ILGenerator gen = dyn.GetILGenerator();
    gen.Emit(OpCodes.Ldarg_0);
    gen.Emit(OpCodes.Ldarg_1);
    if (fld.FieldType.IsClass)
    {
        gen.Emit(OpCodes.Castclass, fld.FieldType);
    }
    else
    {
        gen.Emit(OpCodes.Unbox_Any, fld.FieldType);
    }
    gen.Emit(OpCodes.Stfld, fld);
    gen.Emit(OpCodes.Ret);
    return (Action<MyType, object>)dyn.CreateDelegate(typeof(Action<MyType, object>));
}


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow