Laden...

DynamicFieldAccessor statt Reflection - schneller dynamischer Zugriff auf Felder/Properties

Erstellt von dN!3L vor 16 Jahren Letzter Beitrag vor 10 Jahren 23.932 Views
dN!3L Themenstarter:in
2.891 Beiträge seit 2004
vor 16 Jahren
DynamicFieldAccessor statt Reflection - schneller dynamischer Zugriff auf Felder/Properties

oder: Erzeugung dynamischer Typen als Ersatz für GetValue/SetValue mittels Reflection für häufige, performanzkritische Feld-/Propertyzugriffe.

Beschreibung:
Relektion ist toll 😄. Mit Reflektion kann man z.B. die Felder und Properties von Typen auslesen und belegen. Ein Beispiel ist z.B. ein O/R-Mapper, der die Werte einer Datenbankzeile ausliest und entsprechend des Spaltennamens Felder/Properties eines Typs mit den jeweiligen Werten belegt.
Dabei werden jedoch oft mehrere hundert oder tausend Objekte erzeugt. Und Reflektion ist langsam. Deshalb bietet es sich an, dynamisch einen Typen zu erzeugen, der die entsprechenden Felder/Properties nicht mehr via Reflection-API liest/setzt, sondern dies quasi mit einem direkten Verweis macht. Die Grundidee ist aus CodeProject: Fast Dynamic Property Access, die hier gezeigte Klasse ist aber noch ergänzt um Typkonvertierung für den Anwendungsfall Einfacher Tabellen Mapper . Zusätzlich werden Felder unterstützt. Da Microsoft so genial war und den Begriff "(Klassen-)Attribute" überladen hat, heißt meine Klasse "DynamicFieldAccessor" (Properties sind auch nur abstrahierte Felder...).

Features:*Lesen und Schreiben von Feldern und Properties *auch protected Felder/Properties können gelesen/geschrieben werden *Unterstützung von Enums und Nullable-Types *(normales) Lesen und Schreiben bis etwa 50x so schnell als Reflection *unterstützt Typkonvertierung beim Schreiben (optional) *beim Schreiben mit Typkonvertierung sogar schneller als direkter Zugriff

Beispiel:

public class TestClass<T>
{
	public T Value { get; set; }
}
TestClass<int> testClass = new TestClass<int>();

// direkter Zugriff
int value = testClass.Value;
testClass.Value = value;

// Zugriff mit Reflection-API
PropertyInfo propertyInfo = typeof(TestClass<int>).GetProperty("Value");
value = (int)propertyInfo.GetValue(testClass);
propertyInfo.SetValue(testClass,value);

// Zugriff mit DynamicFieldAccessor
propertyInfo = typeof(TestClass<int>).GetProperty("Value");
IDynamicFieldAccessor fieldAccessor = DynamicFieldAccessor.Create(propertyInfo,false);
value = fieldAccessor.Get(testClass);
fieldAccessor.Set(testClass,value);

DynamicFieldAccessor.Create(...) erzeugt dabei je nach Property-/Feld dynamisch einen neuen Typ (hier eine Klasse), die den Property/Feldzugriff quasi direkt durchführt. Im Folgenden sieht man für verschiedene Anwendungsfälle den C#-Code, der dem erzeugten IL-Code/dynamischen Typen entspricht: *IDynamicFieldAccessor fieldAccessor = DynamicFieldAccessor.Create(typeof(TestClass<string>).GetProperty ("Value"),false);

public class TestClass_SystemString_Value : IDynamicFieldAccessor
{
    public virtual object Get(object target)
    {
        return ((TestClass<string>) target).Value;
    }

    public virtual void Set(object target, object value)
    {
        ((TestClass<string>) target).Value = (string) value;
    }
}

*IDynamicFieldAccessor fieldAccessor = DynamicFieldAccessor.Create(typeof(TestClass<int>).GetProperty ("Value"),true);

public class TestClass_Int_Value_TypeChange : IDynamicFieldAccessor
{
    public object Get(object target)
    {
        return ((TestClass<int>) target).Value;
    }

    public void Set(object target, object value)
    {
        if ((value is int) || typeof(int).IsAssignableFrom(value.GetType()))
            ((TestClass<int>) target).Category = (int) value;
        else
            ((TestClass<int>) target).Category = Convert.ToInt32(value);
    }
}

*IDynamicFieldAccessor fieldAccessor = DynamicFieldAccessor.Create(typeof(TestClass<TestEnum>).GetProperty ("Value"),true);

public class TestClass_TestEnum_Value : IFieldAccessor
{
    public virtual object Get(object target)
    {
        return ((TestClass<TestEnum>) target).Value;
    }

    public virtual void Set(object target, object value)
    {
        if (value is int)
            ((TestClass<TestEnum>) target).Value = (TestEnum) value;
        else
            ((TestClass<TestEnum>) target).Value = (TestEnum) Enum.Parse(typeof(TestEnum), value.ToString());
    }
}

*IDynamicFieldAccessor fieldAccessor = DynamicFieldAccessor.Create(typeof(TestClass<int?>).GetProperty ("Value"),true);

public class TestClass_SystemNullableSystemInt_Value : IDynamicFieldAccessor
{
    public virtual object Get(object target)
    {
        return ((TestClass<int?>) target).Value;
    }

    public virtual void Set(object target, object value)
    {
        if ((value is int?) || typeof(int?).IsAssignableFrom(value.GetType()))
            ((TestClass<int?>) target).Value = (int?) value;
        else
            ((TestClass<int?>) target).Value = (int?) Convert.ChangeType(value, Nullable.GetUnderlyingType(typeof(int?)));
    }
}

Performanzvergleich:

Teste System.Int32 mit 500000 Wiederholungen
   Getter:
      direkter Zugriff                      19
      DynamicFieldAccessor                  28 #
      FastDynamicPropertyAccessor           47
      Reflection/PropertyInfo.GetValue    1569
   Setter (ohne Typkovertierungsprüfung):
      direkter Zugriff                      70
      DynamicFieldAccessor                  31 #
      FastDynamicPropertyAccessor           43
      Reflection/PropertyInfo.SetValue    1971
   Setter (mit Typkonvertierungprüfung,ohne Typänderung):
      direkter Zugriff                      55
      DynamicFieldAccessor                  34 #
      FastDynamicPropertyAccessor           70
      Reflection/PropertyInfo.SetValue    2006
   Setter (mit Typkonvertierungprüfung,mit Typänderung):
      direkter Zugriff                     771
      DynamicFieldAccessor                 394 #
      FastDynamicPropertyAccessor          763
      Reflection/PropertyInfo.SetValue    2947

Teste System.String mit 500000 Wiederholungen
   Getter:
      direkter Zugriff                      17
      DynamicFieldAccessor                  31 #
      FastDynamicPropertyAccessor           44
      Reflection/PropertyInfo.GetValue    1439
   Setter (ohne Typkovertierungsprüfung):
      direkter Zugriff                      35
      DynamicFieldAccessor                  41 #
      FastDynamicPropertyAccessor           47
      Reflection/PropertyInfo.SetValue    1835
   Setter (mit Typkonvertierungprüfung,ohne Typänderung):
      direkter Zugriff                      41
      DynamicFieldAccessor                  24 #
      FastDynamicPropertyAccessor           62
      Reflection/PropertyInfo.SetValue    1893
   Setter (mit Typkonvertierungprüfung,ohne Typänderung):
      direkter Zugriff                      51
      DynamicFieldAccessor                  28 #
      FastDynamicPropertyAccessor           64
      Reflection/PropertyInfo.SetValue    1877

Teste System.Nullable`1[[System.Int32]] mit 500000 Wiederholungen
   Getter:
      direkter Zugriff                      23
      DynamicFieldAccessor                 474 #
      FastDynamicPropertyAccessor          587
      Reflection/PropertyInfo.GetValue    2323
   Setter (ohne Typkovertierungsprüfung):
      direkter Zugriff                     286
      DynamicFieldAccessor                 730 #
      FastDynamicPropertyAccessor          781
      Reflection/PropertyInfo.SetValue    3067
   Setter (mit Typkonvertierungprüfung,mit Typänderung):
      direkter Zugriff                     463
      DynamicFieldAccessor                 258 #
      FastDynamicPropertyAccessor          658
      Reflection/PropertyInfo.SetValue    2880
   Setter (mit Typkonvertierungprüfung,mit Typänderung):
      direkter Zugriff                    2907
      DynamicFieldAccessor                2154 #
      FastDynamicPropertyAccessor         2880
      Reflection/PropertyInfo.SetValue    6071

Code:
Und nun endlich der Code:

/// <summary>
/// Ermöglicht den schnellen Zugriff (ohne Reflection) auf Felder/Properties
/// </summary>
public interface IDynamicFieldAccessor
{
	/// <summary>
	/// Ersetzt das Property/Feld, das durch diese Instanz repräsentiert wird, am übergeben Zielobjekt durch den übergeben Wert
	/// </summary>
	/// <param name="target">das Zielobjekt, dem das Feld/Property angehört</param>
	/// <param name="value">der Wert, der gesetzt werden soll</param>
	void Set(object target,object value);


	/// <summary>
	/// Liest das Property/Feld, das durch diese Instanz repräsentiert wird, vom übergeben Zielobjekt aus
	/// </summary>
	/// <param name="target">das Zielobjekt, dem das Feld/Property angehört</param>
	/// <returns>den Wert des Properties/Feldes, das durch diese Instantz repräsentiert wird</returns>
	object Get(object target);
}

/*using System;
  using System.Collections.Generic;
  using System.Reflection;
  using System.Reflection.Emit;
  using System.Runtime.Serialization;
  using System.Text.RegularExpressions;*/


/// <summary>
/// Stellt Methoden bereit, um Objekte zu erzeugen, die den schnellen Zugriff (ohne Reflection) auf Felder/Properties ermöglichen
/// </summary>
public abstract class DynamicFieldAccessor
{
	// Cache zum Speichern der bereits erzeugten "dynamischen Feldzugreifer"
	private static Dictionary<bool,Dictionary<MemberInfo,IDynamicFieldAccessor>> cache = new Dictionary<bool,Dictionary<MemberInfo,IDynamicFieldAccessor>>();

	// MethodInfos, die oft benutzt werden
	private static readonly MethodInfo typeGetTypeFromHandleMethodInfo = typeof(Type).GetMethod("GetTypeFromHandle");
	private static readonly MethodInfo objectGetTypeMethodInfo = typeof(object).GetMethod("GetType");
	private static readonly MethodInfo typeIsAssignableFromMethodInfo = typeof(Type).GetMethod("IsAssignableFrom");
	private static readonly MethodInfo convertChangeTypeMethodInfo = typeof(Convert).GetMethod("ChangeType",new Type[]{typeof(object),typeof(Type)});
	private static readonly MethodInfo objectToStringMethodInfo = typeof(object).GetMethod("ToString");
	private static readonly MethodInfo enumParseMethodInfo = typeof(Enum).GetMethod("Parse",new Type[]{typeof(Type),typeof(string)});
	private static readonly MethodInfo nullableGetUnderlyingTypeMethodInfo = typeof(Nullable).GetMethod("GetUnderlyingType");



	/// <summary>
	/// Konstruktor
	/// </summary>
	static DynamicFieldAccessor()
	{
		// Felder im Cache anlegen
		DynamicFieldAccessor.cache.Add(true,new Dictionary<MemberInfo,IDynamicFieldAccessor>());
		DynamicFieldAccessor.cache.Add(false,new Dictionary<MemberInfo,IDynamicFieldAccessor>());
	}



	/// <summary>
	/// Erzeugt eine neue IDynamicFieldAccessor-Instanz
	/// </summary>
	/// <param name="memberInfo">das Feld/Property, auf das zugegriffen werden soll</param>
	/// <param name="enableSetterTypeChange">true, falls eine (eventuelle) Typkonvertierung beim Setzen von Werten vorgenommen werden soll</param>
	/// <returns>eine neue IDynamicFieldAccessor-Instanz zum Zugriff auf das übergebene Feld/Property</returns>
	public static IDynamicFieldAccessor Create(MemberInfo memberInfo,bool enableSetterTypeChange)
	{
		if (!(memberInfo is PropertyInfo || memberInfo is FieldInfo))
			throw new ArgumentException(memberInfo+" ist kein Feld oder Property.");
		else
			// Instanz aus dem Cache zurückgeben
			if (DynamicFieldAccessor.cache[enableSetterTypeChange].ContainsKey(memberInfo))
				return DynamicFieldAccessor.cache[enableSetterTypeChange][memberInfo];
			// neue Instanz erzeugen und im Cache speichern
			else
			{
				IDynamicFieldAccessor fieldAccessor = DynamicFieldAccessor.create(memberInfo,enableSetterTypeChange);
				DynamicFieldAccessor.cache[enableSetterTypeChange].Add(memberInfo,fieldAccessor);
				return fieldAccessor;
			}
	}



	/// <summary>
	/// Erzeugt eine neue IDynamicFieldAccessor-Instanz
	/// </summary>
	/// <param name="memberInfo">das Feld/Property, auf das zugegriffen werden soll</param>
	/// <param name="enableSetterTypeChange">true, falls eine (eventuelle) Typkonvertierung beim Setzen von Werten vorgenommen werden soll</param>
	/// <returns>eine neue IDynamicFieldAccessor-Instanz zum Zugriff auf das übergebene Feld/Property</returns>
	private static IDynamicFieldAccessor create(MemberInfo memberInfo,bool enableSetterTypeChange)
	{
		// Pipeline AssemblyBuilder->ModuleBuilder->TypeBuilder
#if DEBUG
		string name = "[DynamicFieldAccessor]"+memberInfo.ReflectedType.Name+" "+Regex.Replace(memberInfo.ToString().Replace(" ","_"),"[^a-z^A-Z^_]","");
		if (enableSetterTypeChange)
			name += "(TypeChange)";
		AssemblyName assemblyName = new AssemblyName(name);
		AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly (assemblyName,AssemblyBuilderAccess.RunAndSave);
		ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(name,name+".dll");
#else
		string name = Guid.NewGuid().ToString();
		AssemblyName assemblyName = new AssemblyName(name);
		AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly (assemblyName,AssemblyBuilderAccess.Run);
		ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(name);
#endif
		// einen neuen TypeBuilder erzeugen. Falls das Feld/Property nicht public ist, muss vom reflektierten Typ geerbt werden, damit Zugriff erhalten werden kann
		TypeBuilder typeBuilder = (DynamicFieldAccessor.mustInherit(memberInfo)) ? moduleBuilder.DefineType(name,TypeAttributes.Public,memberInfo.ReflectedType) : moduleBuilder.DefineType(name,TypeAttributes.Public);


		// der neue Typ implementiert die Schnittstelle IDynamicFieldAccessor; die hat eine Methode "Get" und eine Methode "Set", welche implementiert werden müssen
		typeBuilder.AddInterfaceImplementation(typeof(IDynamicFieldAccessor));
		DynamicFieldAccessor.implementGetMethod(typeBuilder,memberInfo);
		DynamicFieldAccessor.implementSetMethod (typeBuilder,memberInfo,enableSetterTypeChange);


		// den Typen erzeugen
		Type resultType = typeBuilder.CreateType();
#if DEBUG
		// die Assembly auf die Platte speichern, damit man den produzierten IL-/C#-Code angucken kann
		assemblyBuilder.Save(name+".dll");
#endif
		// neue Instanz erzeugen -> dabei wird Konstruktor nicht aufgerufen (braucht nicht; hätte bei Spezialisierungen, um an protected Member zu kommen, auch Nebeneffekte)
		IDynamicFieldAccessor result = (IDynamicFieldAccessor)FormatterServices.GetUninitializedObject(resultType);
		return result;
	}



	/// <summary>
	/// Prüft, ob ein neuer Typ vom Typ, der das Property/Feld enthält, ableiten muss, um an das Property/Feld zu kommen, da dies nicht public ist
	/// </summary>
	/// <param name="memberInfo">der Member, der untersucht werden soll</param>
	/// <returns>true, falls abgeleitet werden muss</returns>
	private static bool mustInherit(MemberInfo memberInfo)
	{
		PropertyInfo propertyInfo = memberInfo as PropertyInfo;
		FieldInfo fieldInfo = memberInfo as FieldInfo;

		// Es muss abgeleitet werden, wenn Getter oder Setter des Properties oder das Feld nicht public ist.
		// Getter/Setter/Feld müssen natürlich auch protected sein

		// wenn es ein Property ist
		if (propertyInfo!=null)
		{
			MethodInfo getMethod = propertyInfo.GetGetMethod(true);
			if (getMethod!=null)
			{
				if ((getMethod.Attributes & MethodAttributes.Family)!=MethodAttributes.Family)
					throw new ArgumentException("'"+memberInfo+"' muss public oder protected sein.");
				if ((getMethod.Attributes & MethodAttributes.Public)!=MethodAttributes.Public)
					return true;
			}

			MethodInfo setMethod = propertyInfo.GetSetMethod(true);
			if (setMethod!=null)
			{
				if ((setMethod.Attributes & MethodAttributes.Family)!=MethodAttributes.Family)
					throw new ArgumentException("'"+memberInfo+"' muss public oder protected sein.");
				if ((setMethod.Attributes & MethodAttributes.Public)!=MethodAttributes.Public)
					return true;
			}
		}
		// wenn es ein Feld ist
		else
		{
			if ((fieldInfo.Attributes & FieldAttributes.Family)!=FieldAttributes.Family)
				throw new ArgumentException("'"+memberInfo+"' muss public oder protected sein.");
			if ((fieldInfo.Attributes & FieldAttributes.Public)!=FieldAttributes.Public)
				return true;
		}

		return false;
	}



	/// <summary>
	/// Implementiert die "object Get(object target)"-Methode von IDynamicFieldAccessor
	/// </summary>
	/// <param name="typeBuilder">der zu verwendende TypeBuilder</param>
	/// <param name="memberInfo">das Feld/Property, auf das ausgelesen werden soll</param>
	private static void implementGetMethod(TypeBuilder typeBuilder,MemberInfo memberInfo)
	{
		PropertyInfo propertyInfo = memberInfo as PropertyInfo;
		FieldInfo fieldInfo = memberInfo as FieldInfo;

		// neue Methode erstellen und die Methode aus der Schnittstellendeklaration damit überschreiben
		MethodInfo methodInfo = typeof(IDynamicFieldAccessor).GetMethod("Get");
		MethodBuilder methodBuilder = typeBuilder.DefineMethod(methodInfo.Name,(MethodAttributes) (methodInfo.Attributes-MethodAttributes.Abstract),methodInfo.ReturnType,new Type[] { typeof(object) });
		typeBuilder.DefineMethodOverride(methodBuilder,methodInfo);

		// IL-Generator erzeugen
		ILGenerator ilGenerator = methodBuilder.GetILGenerator();

		if (propertyInfo!=null && propertyInfo.GetGetMethod(true)==null)
		{
			// wenn es ein Property ist und kein Getter vorhanden ist, Exception werfen
			ilGenerator.Emit(OpCodes.Ldstr,"Property '"+memberInfo+"' hat keinen Getter.");
			ilGenerator.Emit(OpCodes.Newobj,typeof(NotSupportedException).GetConstructor(new Type[] { typeof(string) }));
			ilGenerator.Emit(OpCodes.Throw);
		}
		// Property-/Feldzugriff implementieren
		else
		{
			// lokale Variable
			ilGenerator.DeclareLocal(typeof(object));

			// übergebenes Objekt laden und zum casten
			ilGenerator.Emit(OpCodes.Ldarg_1);
			ilGenerator.Emit(OpCodes.Castclass,memberInfo.ReflectedType);

			if (propertyInfo!=null)
				// Property auslesen
				ilGenerator.Emit(OpCodes.Callvirt,propertyInfo.GetGetMethod(true));
			else
				// Feld auslesen
				ilGenerator.Emit(OpCodes.Ldfld,fieldInfo);

			// den gelesenen Property-/Feldwert boxen
			Type memberType = (propertyInfo!=null) ? propertyInfo.PropertyType : fieldInfo.FieldType;
			if (memberType.IsValueType)
				ilGenerator.Emit(OpCodes.Box,memberType);

			// gelesenen Property-/Feldwert zurückgeben
			ilGenerator.Emit(OpCodes.Ret);
		}
	}



	/// <summary>
	/// Implementiert die "void Set(object target,object value);"-Methode von IDynamicFieldAccessor
	/// </summary>
	/// <param name="typeBuilder">der zu verwendende TypeBuilder</param>
	/// <param name="memberInfo">das Feld/Property, auf das gesetzt werden soll</param>
	/// <param name="changeType">true, falls eine (eventuelle) Typkonvertierung beim Setzen von Werten vorgenommen werden soll</param>
	private static void implementSetMethod(TypeBuilder typeBuilder,MemberInfo memberInfo,bool changeType)
	{
		PropertyInfo propertyInfo = memberInfo as PropertyInfo;
		FieldInfo fieldInfo = memberInfo as FieldInfo;
		Type memberType = (propertyInfo!=null) ? propertyInfo.PropertyType : fieldInfo.FieldType;	// Zieltyp

		// neue Methode erstellen und die Methode aus der Schnittstellendeklaration damit überschreiben
		MethodInfo methodInfo = typeof(IDynamicFieldAccessor).GetMethod("Set");
		MethodBuilder methodBuilder = typeBuilder.DefineMethod(methodInfo.Name,(MethodAttributes) (methodInfo.Attributes-MethodAttributes.Abstract),methodInfo.ReturnType,new Type[] { typeof(object),typeof(object) });
		typeBuilder.DefineMethodOverride(methodBuilder,methodInfo);

		// IL-Generator erzeugen
		ILGenerator ilGenerator = methodBuilder.GetILGenerator();
		
		// Sprungziele definieren
		Label labelSetConverted = ilGenerator.DefineLabel();
		Label labelSetDirect = ilGenerator.DefineLabel();

		if (propertyInfo!=null && propertyInfo.GetSetMethod(true)==null)
		{
			// wenn es ein Property ist und kein Setter vorhanden ist, Exception werfen
			ilGenerator.Emit(OpCodes.Ldstr,"Property '"+memberInfo+"' hat keinen Setter.");
			ilGenerator.Emit(OpCodes.Newobj,typeof(NotSupportedException).GetConstructor(new Type[]{typeof(string)}));
			ilGenerator.Emit(OpCodes.Throw);
		}
		// Property-/Feldzugriff implementieren
		else
		{
			// wenn gewünscht, die TypPRÜFUNG implementieren
			if (changeType)
			{
				// lokale Variable
				ilGenerator.DeclareLocal(typeof(bool));

				// den zu setzende Wert laden; {value is MemberType}-Vergleich machen; falls erfolgreich, zum direkt ohne Konvertieren setzen gehen
				ilGenerator.Emit(OpCodes.Ldarg_2);
				ilGenerator.Emit(OpCodes.Isinst,(!memberType.IsEnum) ? memberType : typeof(int));
				ilGenerator.Emit(OpCodes.Brtrue_S,labelSetDirect);

				// is-Vergleich ist nicht erfolgreich: bei Enumtypen zum Konvertieren gehen...
				if (memberType.IsEnum)
					ilGenerator.Emit(OpCodes.Br_S,labelSetConverted);
				// ... ansonsten noch Testen, ob der Wert nicht doch zuweisbar ist
				else
				{
					// {if (typeof(MemberType).IsAssignableFrom(value.GetType()))}
					ilGenerator.Emit(OpCodes.Ldtoken,memberType);
					ilGenerator.Emit(OpCodes.Call,typeGetTypeFromHandleMethodInfo);
					ilGenerator.Emit(OpCodes.Ldarg_2);
					ilGenerator.Emit(OpCodes.Callvirt,objectGetTypeMethodInfo);
					ilGenerator.Emit(OpCodes.Callvirt,typeIsAssignableFromMethodInfo);

					// wenn der Test nicht erfolgreich war, zum Konvertieren gehen...
					ilGenerator.Emit(OpCodes.Brfalse_S,labelSetConverted);
					// ... ansonsten kommt jetzt folgend die Zuweisung ohne Konvertieren
				}

				ilGenerator.MarkLabel(labelSetDirect);
			}

			// Zielobjekt laden; dieses casten; Wert laden; unboxen
			ilGenerator.Emit(OpCodes.Ldarg_1);
			ilGenerator.Emit(OpCodes.Castclass,memberInfo.ReflectedType);
			ilGenerator.Emit(OpCodes.Ldarg_2);
			ilGenerator.Emit(OpCodes.Unbox_Any,memberType);

			if (propertyInfo!=null)
				// Property setzen
				ilGenerator.Emit(OpCodes.Callvirt,propertyInfo.GetSetMethod(true));
			else
				// Feld setzen
				ilGenerator.Emit(OpCodes.Stfld,fieldInfo);

			// fertig
			ilGenerator.Emit(OpCodes.Ret); 

			// falls gewünscht, hier der Teil zum Konvertieren
			if (changeType)
			{
				ilGenerator.MarkLabel(labelSetConverted);

				// Zielobjekt laden und casten
				ilGenerator.Emit(OpCodes.Ldarg_1);
				ilGenerator.Emit(OpCodes.Castclass,memberInfo.ReflectedType);

				if (!memberType.IsEnum)
				{
					// value laden
					ilGenerator.Emit(OpCodes.Ldarg_2);

					MethodInfo convertToMethodInfo = DynamicFieldAccessor.getConvertToMethodInfo(memberType);
					if (convertToMethodInfo!=null)
						// {Convert.ToXY(value)}
						ilGenerator.Emit(OpCodes.Call,convertToMethodInfo);
					else
					{
						// {Convert.ChangeType(value,typeof(MemberType))}
						// Zieltyp laden
						ilGenerator.Emit(OpCodes.Ldtoken,memberType);
						ilGenerator.Emit(OpCodes.Call,typeGetTypeFromHandleMethodInfo);
						if (memberType.IsGenericType && memberType.GetGenericTypeDefinition()==typeof(Nullable<>))
							// {Convert.ChangeType(value,Nullable.GetUnderlyingType(typeof(MemberType)))}
							// GetUnderlyingType aufrufen
							ilGenerator.Emit(OpCodes.Call,nullableGetUnderlyingTypeMethodInfo);
						// .ToXY/.ChangeType aufrufen
						ilGenerator.Emit(OpCodes.Call,convertChangeTypeMethodInfo);
						ilGenerator.Emit(OpCodes.Unbox_Any,memberType);
					}
				}
				// Enums
				else
				{
					// Enum-Zieltyp laden
					ilGenerator.Emit(OpCodes.Ldtoken,memberType);
					ilGenerator.Emit(OpCodes.Call,typeGetTypeFromHandleMethodInfo);
					// value laden und zu string machen
					ilGenerator.Emit(OpCodes.Ldarg_2);
					ilGenerator.Emit(OpCodes.Callvirt,objectToStringMethodInfo);
					// {Enum.Parse(typeof(MemberType),value)}
					ilGenerator.Emit(OpCodes.Call,enumParseMethodInfo);
					ilGenerator.Emit(OpCodes.Unbox_Any,memberType);
				}

				if (propertyInfo!=null)
					// Property setzen
					ilGenerator.Emit(OpCodes.Callvirt,propertyInfo.GetSetMethod(true));
				else
					// Feld setzen
					ilGenerator.Emit(OpCodes.Stfld,fieldInfo);

				// fertig
				ilGenerator.Emit(OpCodes.Ret);
			}
		}
	}



	/// <summary>
	/// Gibt das MethodInfo der zum übergebenen Typ gehörenden Convert.ToXY-Method zurück (null, falls keine passende gefunden)
	/// </summary>
	/// <param name="targetType">Typ, in den konvertiert werden soll</param>
	/// <returns>das MethodInfo der zum übergebenen Typ gehörenden Convert.ToXY-Method zurück (null, falls keine passende gefunden)</returns>
	private static MethodInfo getConvertToMethodInfo(Type targetType)
	{
		// Signatur der gesuchen Methode ermitteln
		string methodName;
		if (targetType==typeof(Boolean))
			methodName = "ToBoolean";
		else if (targetType==typeof(Byte))
			methodName = "ToByte";
		else if (targetType==typeof(Char))
			methodName = "ToChar";
		else if (targetType==typeof(DateTime))
			methodName = "ToDateTime";
		else if (targetType==typeof(Decimal))
			methodName = "ToDecimal";
		else if (targetType==typeof(Double))
			methodName = "ToDouble";
		else if (targetType==typeof(Int16))
			methodName = "ToInt16";
		else if (targetType==typeof(Int32))
			methodName = "ToInt32";
		else if (targetType==typeof(Int64))
			methodName = "ToInt64";
		else if (targetType==typeof(SByte))
			methodName = "ToSByte";
		else if (targetType==typeof(Single))
			methodName = "ToSingle";
		else if (targetType==typeof(String))
			methodName = "ToString";
		else if (targetType==typeof(UInt16))
			methodName = "ToUInt16";
		else if (targetType==typeof(UInt32))
			methodName = "ToUInt32";
		else if (targetType==typeof(UInt64))
			methodName = "ToUInt64";
		else
			return null;
		
		// Methode mit der entsprechenden Signatur suchen und zurückgeben
		return typeof(Convert).GetMethod(methodName,new Type[]{typeof(object)});
	}
}

Schlagwörter: DynamicFieldAccessor, Felder, Properties, Reflection, IL-Generator

letzte Änderung am Code: 27.03.2008

O
778 Beiträge seit 2007
vor 16 Jahren

Blöde Frage: Warum benutzt du nicht einfach System.Reflection.Emit.DynamicMethod? So hab ich's bei meinen Cheatmeisen gemacht (AntMe), die dann in der Lage waren sich selbst zu heilen und die Käfer für tot zu erklären 🙂 :evil:

Aber Spaß beiseite, mit DynamicMethod geht das ganze viel einfacher, da du m.E. nach einfach an den bestehenden Typen zur Laufzeit quasi eine Methode dranhaengst. Der Zugriff erfolgt dann per Delegate, aber den kann man ja kapseln.

Und ein bisschen (konstruktive) Kritik hab ich auch noch übrig 🙂 Du rufst Properties immer ueber callvirt auf, das ist aber nur bei virtuellen Properties notwendig.

dN!3L Themenstarter:in
2.891 Beiträge seit 2004
vor 16 Jahren

Hallo,

Blöde Frage: Warum benutzt du nicht einfach System.Reflection.Emit.DynamicMethod? Aber Spaß beiseite, mit DynamicMethod geht das ganze viel einfacher, da du m.E. nach einfach an den bestehenden Typen zur Laufzeit quasi eine Methode dranhaengst. Der Zugriff erfolgt dann per Delegate, aber den kann man ja kapseln.

Hm, am besten mache ich eine Liste:*Kannte ich nicht (woher kennst du dich damit aus?) *Naja, "einfacher" werde ich es nicht mehr haben. Die Klasse ist fertig und wird eingesetzt 😁

*Das "einfacher" würde ich auch erstmal nur auf die Programmierung beziehen. Denn von der Struktur her kann ich nicht erkennen, dass es einfacher, als die Klasse/das Interface ist (wäre eher noch zusätzlicher Aufwand) *Es geht hier primär um Performanz. Ich würde einfach mal in den Raum stellen, dass so ein Delegate-Zugriff langsamer ist. Wenn ich mal ne freie Minute habe, werd ich mir DynamicMethod aber mal genauer angucken und testen. *Summa summarum sehe ich als einzigen Vorteil, dass ich die Methode einfach in ein bestehendes Modul hänge und man so auch leichter Zugriff auf nichtöffentliches Zeugs hat (Was meine Lösung aber auch kann).

Und ein bisschen (konstruktive) Kritik hab ich auch noch übrig 🙂 Du rufst Properties immer ueber callvirt auf, das ist aber nur bei virtuellen Properties notwendig. Hm, ich hab "Reverse-Programming" betrieben. Der C#-Compiler hat das so ausgegeben. Performanzmäßig bringt ein Call aber auch nichts.

Gruß
dN!3L

J
32 Beiträge seit 2008
vor 16 Jahren

Sehr interessant, werde das ganze gleich mal in meinen einfachen Tabellen Mapper integrieren. Damit dürfte ich Performanzmäßig noch einiges herausholen können 🙂.

Die DynamicMethod Variante habe ich mir auch schon angesehen.
Ich befürchte auch, dass diese Vorgehensweise langsamer ist.

O
778 Beiträge seit 2007
vor 16 Jahren

Ich hab die Performance von DynamicMethod ehrlich gesagt noch nie getestet, kann mir aber nicht wirklich vorstellen, dass der Delegate-Zugriff soo viel langsamer ist als der Zugriff über Interface. Momentan kann ich's auch nicht überprüfen (Lüfter vom Laptop kaputt X()

F
722 Beiträge seit 2005
vor 16 Jahren

Das ist ne tolle Sache, ich habe mir direkt mal die DynamicMethod Variante zusammengebastelt.
Aber leider lohnt sich das ganze nur, wenn ich sehr oft ein und das selbe Property schreibe oder lese. Ansonsten ist der Aufwand zu groß, den Code zu erzeugen.
Wie auch immer, trotzdem ne gute Sache!
Gruß

dN!3L Themenstarter:in
2.891 Beiträge seit 2004
vor 16 Jahren

Hallo,

Das ist ne tolle Sache, ich habe mir direkt mal die DynamicMethod Variante zusammengebastelt. Könntest du mal bitte den Code posten (bzw. mit zukommen lassen)? Dann kann ich mal ein paar Performanztests machen (und muss nicht so viel vorbereiten...).

Aber leider lohnt sich das ganze nur, wenn ich sehr oft ein und das selbe Property schreibe oder lese. Ja, genau dafür ist es doch gedacht. 😁

Gruß
dN!3L

F
722 Beiträge seit 2005
vor 16 Jahren

Hier die DynamicMethod Lösung:


public class DynamicAccessor
    {
        delegate object Getter(object target);
        delegate void Setter(object target, object value);

        Getter _getter;
        Setter _setter; 

        public DynamicAccessor(PropertyInfo propertyInfo)
        {
            _getter = EmitGetter(propertyInfo);

            if (propertyInfo.CanWrite)
            {
                _setter = EmitSetter(propertyInfo);
            }
        }

        Getter EmitGetter(PropertyInfo propertyInfo)
        {
            DynamicMethod dynMethod = new DynamicMethod("Get", 
                typeof(object), 
                new Type[]{typeof(object)}, 
                propertyInfo.DeclaringType);

            ILGenerator il = dynMethod.GetILGenerator();
            MethodInfo getterMethod = propertyInfo.GetGetMethod();

            il.DeclareLocal(typeof(object)); 
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
            il.EmitCall(OpCodes.Callvirt, getterMethod, null);

            OpCode box = propertyInfo.PropertyType.IsValueType ? 
                OpCodes.Box : 
                OpCodes.Castclass;            

            il.Emit(box, propertyInfo.PropertyType);

            il.Emit(OpCodes.Ret);                                   

            return (Getter)dynMethod.CreateDelegate(typeof(Getter)); 
        }

        Setter EmitSetter(PropertyInfo propertyInfo)
        {
            DynamicMethod dynMethod = new DynamicMethod("Set",
                null,
                new Type[]{typeof(object), 
                           typeof(object)},
                propertyInfo.DeclaringType);

            ILGenerator il = dynMethod.GetILGenerator();
            MethodInfo setter = propertyInfo.GetSetMethod();
            
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Castclass, propertyInfo.DeclaringType); 
            il.Emit(OpCodes.Ldarg_1); 
            
            OpCode box = propertyInfo.PropertyType.IsValueType ? 
                OpCodes.Unbox_Any : 
                OpCodes.Castclass;  
           
            il.Emit(box, propertyInfo.PropertyType); 
            il.EmitCall(OpCodes.Callvirt, setter, null);

            il.Emit(OpCodes.Ret);            

            return (Setter)dynMethod.CreateDelegate(typeof(Setter)); 
        }        

        public void Set(object target, object value)
        {
            if (_setter == null)
                throw new NotSupportedException("Property is read-only");

            _setter(target, value); 
        }

        public object Get(object target)
        {
           return  _getter(target); 
        }
    }


Ich suche jetzt nach einer Möglichkeit, die Zieltypen im IL Code zu verändern, dann könnte man das ganze dynamisch für verschiedene Properties verwenden. DynamicILInfo könnte dabei helfen.
Hat jemand damit schon Erfahrungen gemacht?

dN!3L Themenstarter:in
2.891 Beiträge seit 2004
vor 16 Jahren

Hier die DynamicMethod Lösung:

Und hier die Performanzprüfungen:

Die erste DynamicMethod-Variante ist direkt das Getter-Delegate aufzurufen, die zweite ist die, die du gepostet hast (also quasi noch ein Proxy).
Man sieht, die DynamicMethod-Variante liegt nur ganz knapp hinter der DynamicType-Variante. Hätte ich nicht gedacht...

Teste System.Int32 mit 1000000 Wiederholungen
   Getter:
      direkter Zugriff                      29
      DynamicFieldAccessor                  43
      DynamicMethod Getter                  46
      DynamicMethodPropertyAccessor         45
      FastDynamicPropertyAccessor           46
      Reflection/PropertyInfo.GetValue    2058
   Setter (ohne Typkovertierungsprüfung):
      direkter Zugriff                      27
      DynamicFieldAccessor                  43
      DynamicMethod Setter                  52
      DynamicMethodPropertyAccessor         55
      FastDynamicPropertyAccessor           56
      Reflection/PropertyInfo.SetValue    2654

Teste System.String mit 1000000 Wiederholungen
   Getter:
      direkter Zugriff                      17
      DynamicFieldAccessor                  35
      DynamicMethod Getter                  36
      DynamicMethodPropertyAccessor         37
      FastDynamicPropertyAccessor           39
      Reflection/PropertyInfo.GetValue    1906
   Setter (ohne Typkovertierungsprüfung):
      direkter Zugriff                      33
      DynamicFieldAccessor                  40
      DynamicMethod Setter                  42
      DynamicMethodPropertyAccessor         46
      FastDynamicPropertyAccessor           48
      Reflection/PropertyInfo.SetValue    2443

Teste System.Nullable`1[[System.Int32]] mit 1000000 Wiederholungen
   Getter:
      direkter Zugriff                      32
      DynamicFieldAccessor                 644
      DynamicMethod Getter                 641
      DynamicMethodPropertyAccessor        630
      FastDynamicPropertyAccessor          659
      Reflection/PropertyInfo.GetValue    3157
   Setter (ohne Typkovertierungsprüfung):
      direkter Zugriff                     324
      DynamicFieldAccessor                 945
      DynamicMethod Setter                 951
      DynamicMethodPropertyAccessor        961
      FastDynamicPropertyAccessor          988
      Reflection/PropertyInfo.SetValue    4022

Ich suche jetzt nach einer Möglichkeit, die Zieltypen im IL Code zu verändern, dann könnte man das ganze dynamisch für verschiedene Properties verwenden. 🤔 Was willst du für Zieltypen verändern, und warum?
EDIT: (Folgebeitrag für diese Frage ist Properties dynamisch schneller schreiben als mit PropertyInfo.SetValue )

Gruß
dN!3L

C
1 Beiträge seit 2014
vor 10 Jahren

Hallo allerseits,
auch wenn dieser Beitrag schon älter ist, möchte ich noch eine Verbesserung beisteuern.

Der Zugriff auf Structs wie System.Windows.Media.Media3D.Point3D klappt mit der oben beschriebenen Dynamic Method Lösung nicht. Ich habe mir dafür folgenden Unit Test geschrieben:


        [Test]
        public void CheckStructSecondParameter()
        {
            var Pos = new System.Windows.Media.Media3D.Point3D(10.0,20.0,30.0);
            var expected = 20.0;
            
            var daSubProp = new DynamicAccessor(Pos.GetType().GetProperty("Y"));
            var result = daSubProp.Get(Pos);

            Assert.AreEqual(expected, result);
        }

Dieser Test schlägt fehl! Eigenartig ist jedoch, dass die Variable 'result' den Wert 10.0 hat. Es wird also scheinbar die falsche Property ausgelesen. In diesem Fall passt es zur Property "X".

Wenn man nun statt der Point3D Struktur eine Point3D Klasse anlegt


public class myPoint3D
    {
        public myPoint3D(double x, double y, double z)
        {
            X = x; Y = y; Z = z;
        }
        public double X { get; set; }
        public double Y { get; set; }
        public double Z { get; set; }
    }

und damit den gleichen Test durchführt


        [Test]
        public void CheckClassSecondParameter()
        {
            var Pos = new myPoint3D(10.0, 20.0, 30.0);
            var expected = 20.0;

            var daSubProp = new DynamicAccessor(Pos.GetType().GetProperty("Y"));
            var result = daSubProp.Get(Pos);

            Assert.AreEqual(expected, result);
        }

funktioniert es. Diesmal ist der Test grün.

Bei Stackoverflow habe ich einen Hinweis gefunden: DynamicMethod returns incorrect value when property type is Int64

Ich habe daher die 'EmitGetter' Funktion folgendermaßen angepasst:


        Getter EmitGetter(PropertyInfo propertyInfo)
        {
            DynamicMethod dynMethod = new DynamicMethod("Get",
                typeof(object),
                new Type[] { typeof(object) },
                propertyInfo.DeclaringType);

            ILGenerator il = dynMethod.GetILGenerator();
            MethodInfo getterMethod = propertyInfo.GetGetMethod();

            il.DeclareLocal(typeof(object));
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);

            //// Dieser Codeblock wurde ersetzt. CK,22.1.2014
            //
            // il.EmitCall(OpCodes.Callvirt, getterMethod, null);
            //
            // OpCode box = propertyInfo.PropertyType.IsValueType ?
            //    OpCodes.Box :
            //    OpCodes.Castclass;
            //
            // il.Emit(box, propertyInfo.PropertyType);

            if (propertyInfo.PropertyType.IsValueType)
            {
                if (propertyInfo.ReflectedType.IsValueType)
                {
                    il.Emit(OpCodes.Unbox, propertyInfo.ReflectedType);
                }
                il.EmitCall(OpCodes.Callvirt, getterMethod, null);
                il.Emit(OpCodes.Box, propertyInfo.PropertyType);
            }
            else
            {
                il.EmitCall(OpCodes.Callvirt, getterMethod, null);
                il.Emit(OpCodes.Castclass, propertyInfo.PropertyType);
            }                     
            
            il.Emit(OpCodes.Ret);

            return (Getter)dynMethod.CreateDelegate(typeof(Getter));
        }

Mit dieser Änderung werden die beiden Tests erfolgreich abgeschlossen.

Da ich in meiner Software keinen schreibenden Zugriff benötige, habe ich mir die 'EmitSetter' Funktion nicht näher angesehen. Ich vermute aber, dass dort auch noch Anpassungen notwendig sind.

Viele Grüße,
Christof