2009-08-13 12 views
11

Disons que j'ai le code suivant qui met à jour un champ d'un struct en utilisant la réflexion. Puisque l'instance de structure est copiée dans la méthode DynamicUpdate, it needs to be boxed to an object before being passed.Générer une méthode dynamique pour définir un champ d'une structure au lieu d'utiliser la réflexion

struct Person 
{ 
    public int id; 
} 

class Test 
{ 
    static void Main() 
    { 
     object person = RuntimeHelpers.GetObjectValue(new Person()); 
     DynamicUpdate(person); 
     Console.WriteLine(((Person)person).id); // print 10 
    } 

    private static void DynamicUpdate(object o) 
    { 
     FieldInfo field = typeof(Person).GetField("id"); 
     field.SetValue(o, 10); 
    } 
} 

Le code fonctionne correctement. Maintenant, disons que je ne veux pas utiliser la réflexion parce que c'est lent. Au lieu de cela, je veux générer un CIL modifiant directement le champ id et convertir ce CIL en un délégué réutilisable (disons, en utilisant la fonction Méthode dynamique). Spécialement, je veux remplacer le code ci-dessus s/t comme ceci:

static void Main() 
{ 
    var action = CreateSetIdDelegate(typeof(Person)); 
    object person = RuntimeHelpers.GetObjectValue(new Person()); 
    action(person, 10); 
    Console.WriteLine(((Person)person).id); // print 10 
} 

private static Action<object, object> CreateSetIdDelegate(Type t) 
{ 
    // build dynamic method and return delegate 
}  

Ma question: est-il possible de mettre en œuvre CreateSetIdDelegate d'utiliser une excepts des techniques suivantes?

  1. Générer CIL qui appelle le setter en utilisant la réflexion (en tant que 1er segment de code dans cette publication). Cela n'a aucun sens, étant donné que l'exigence est de se débarrasser de la réflexion, mais c'est une mise en œuvre possible, donc je viens de mentionner.
  2. Au lieu d'utiliser Action<object, object>, utilisez un délégué personnalisé dont la signature est public delegate void Setter(ref object target, object value).
  3. Au lieu d'utiliser Action<object, object>, utilisez Action<object[], object> avec le 1er élément du tableau étant l'objet cible.

La raison pour laquelle je n'aime pas 2 & 3 est parce que je ne veux pas avoir des délégués pour le poseur d'objet et compositeur de struct (et ne pas vouloir faire l'objet set-champ déléguer plus compliqué que nécessaire, par exemple Action<object, object>). Je pense que l'implémentation de CreateSetIdDelegate génèrerait des CIL différents selon que le type cible est struct ou object, mais je veux qu'il renvoie le même délégué offrant la même API à l'utilisateur.

+2

utilise une struct mutable * vraiment * votre meilleure option ici? C'est presque toujours une douleur pour plusieurs raisons, et il semble que vous en rencontriez quelques-uns ... –

+1

Avez-vous envisagé de compiler un arbre d'expression au lieu d'émettre IL? Cela devrait être beaucoup plus facile. –

+0

@Jon: en fait, je suis en train de construire une API de réflexion rapide (http://fasterflect.codeplex.com/), de sorte que la prise en charge des opérations de réflexion par structure serait souhaitable pour certaines personnes. –

Répondre

14

EDIT à nouveau: Ce travaille structures maintenant.

Il y a une manière magnifique de le faire en C# 4, mais vous devrez écrire votre propre code d'émission ILGenerator pour tout ce qui précède. Ils ont ajouté un ExpressionType.Assign au .NET Framework 4.

Cela fonctionne en C# 4 (testé):

public delegate void ByRefStructAction(ref SomeType instance, object value); 

private static ByRefStructAction BuildSetter(FieldInfo field) 
{ 
    ParameterExpression instance = Expression.Parameter(typeof(SomeType).MakeByRefType(), "instance"); 
    ParameterExpression value = Expression.Parameter(typeof(object), "value"); 

    Expression<ByRefStructAction> expr = 
     Expression.Lambda<ByRefStructAction>(
      Expression.Assign(
       Expression.Field(instance, field), 
       Expression.Convert(value, field.FieldType)), 
      instance, 
      value); 

    return expr.Compile(); 
} 

Edit: Voici était mon code de test.

public struct SomeType 
{ 
    public int member; 
} 

[TestMethod] 
public void TestIL() 
{ 
    FieldInfo field = typeof(SomeType).GetField("member"); 
    var setter = BuildSetter(field); 
    SomeType instance = new SomeType(); 
    int value = 12; 
    setter(ref instance, value); 
    Assert.AreEqual(value, instance.member); 
} 
+0

Belle utilisation de la déclaration de C# 4 ET. +1 Mais l'utilisation de * ref * est s/t que je veux éviter (voir la 2ème puce dans ma question) parce que je ne veux pas construire de délégués séparés pour struct setter & class setter. –

+5

Pour une structure, vous devez la passer par ref pour apporter des modifications à l'original. Puisque vous ne pouvez pas ajouter 'ref' ou' out' aux délégués 'Action <>', vous avez les options suivantes: 1) Utiliser une classe au lieu de struct, 2) Utiliser des délégués personnalisés, ou 3) L'ingénier pour que vous ne pas avoir à apporter de modifications à la structure, et passer la valeur de struct by [boxed]. :) –

1

Vous pouvez jeter un oeil à des méthodes dynamiques (la réflexion ne doit pas être lent!) ...

Gerhard a une belle post sur ce que: http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/

+0

C'est ce que je fais (et pose cette question); Je veux remplacer la réflexion en utilisant la méthode dynamique et la question demande si je peux partager la même API de création de délégué pour les deux structs et classes. –

10

Je suis tombé sur un problème similaire, et il m'a fallu plus d'un week-end, mais j'ai finalement pensé à elle après beaucoup de recherche, la lecture, et désassemblage projets de test C#. Et cette version ne nécessite que .NET 2, pas 4.

public delegate void SetterDelegate(ref object target, object value); 
private static Type[] ParamTypes = new Type[] 
{ 
    typeof(object).MakeByRefType(), typeof(object) 
}; 
private static SetterDelegate CreateSetMethod(MemberInfo memberInfo) 
{ 
    Type ParamType; 
    if (memberInfo is PropertyInfo) 
     ParamType = ((PropertyInfo)memberInfo).PropertyType; 
    else if (memberInfo is FieldInfo) 
     ParamType = ((FieldInfo)memberInfo).FieldType; 
    else 
     throw new Exception("Can only create set methods for properties and fields."); 

    DynamicMethod setter = new DynamicMethod(
     "", 
     typeof(void), 
     ParamTypes, 
     memberInfo.ReflectedType.Module, 
     true); 
    ILGenerator generator = setter.GetILGenerator(); 
    generator.Emit(OpCodes.Ldarg_0); 
    generator.Emit(OpCodes.Ldind_Ref); 

    if (memberInfo.DeclaringType.IsValueType) 
    { 
#if UNSAFE_IL 
     generator.Emit(OpCodes.Unbox, memberInfo.DeclaringType); 
#else 
     generator.DeclareLocal(memberInfo.DeclaringType.MakeByRefType()); 
     generator.Emit(OpCodes.Unbox, memberInfo.DeclaringType); 
     generator.Emit(OpCodes.Stloc_0); 
     generator.Emit(OpCodes.Ldloc_0); 
#endif // UNSAFE_IL 
    } 

    generator.Emit(OpCodes.Ldarg_1); 
    if (ParamType.IsValueType) 
     generator.Emit(OpCodes.Unbox_Any, ParamType); 

    if (memberInfo is PropertyInfo) 
     generator.Emit(OpCodes.Callvirt, ((PropertyInfo)memberInfo).GetSetMethod()); 
    else if (memberInfo is FieldInfo) 
     generator.Emit(OpCodes.Stfld, (FieldInfo)memberInfo); 

    if (memberInfo.DeclaringType.IsValueType) 
    { 
#if !UNSAFE_IL 
     generator.Emit(OpCodes.Ldarg_0); 
     generator.Emit(OpCodes.Ldloc_0); 
     generator.Emit(OpCodes.Ldobj, memberInfo.DeclaringType); 
     generator.Emit(OpCodes.Box, memberInfo.DeclaringType); 
     generator.Emit(OpCodes.Stind_Ref); 
#endif // UNSAFE_IL 
    } 
    generator.Emit(OpCodes.Ret); 

    return (SetterDelegate)setter.CreateDelegate(typeof(SetterDelegate)); 
} 

Notez la substance "#if UNSAFE_IL" là-dedans.J'ai trouvé deux façons de le faire, mais la première est vraiment ... hackish. Pour citer l'Ecma-335, le document de normes pour IL:

"Contrairement à la boîte, qui est requise pour faire une copie d'un type de valeur à utiliser dans l'objet, unbox n'est pas nécessaire pour copier le type de valeur de l'objet Typiquement, il calcule simplement l'adresse du type de valeur qui est déjà présent dans l'objet encadré. " Donc, si vous voulez jouer dangereusement, vous pouvez utiliser OpCodes.Unbox pour transformer votre handle d'objet en un pointeur vers votre structure, qui peut ensuite être utilisé comme premier paramètre d'un Stfld ou Callvirt. Le faire de cette façon finit par modifier la structure en place, et vous n'avez même pas besoin de passer votre objet cible par ref. Cependant, notez que la norme ne garantit pas que Unbox vous donnera un pointeur vers la version en boîte. En particulier, il suggère que Nullable peut entraîner la création d'une copie par Unbox. Quoi qu'il en soit, si cela se produit, vous obtiendrez probablement un échec silencieux, où il définit la valeur du champ ou de la propriété sur une copie locale qui est immédiatement rejetée. Donc la façon sûre de le faire est de passer votre objet par ref, de stocker l'adresse dans une variable locale, de faire la modification, puis de rebox le résultat et de le remettre dans votre paramètre d'objet ByRef.

J'ai fait des horaires difficiles, appelant chaque version 10.000.000 fois, avec 2 structures différentes:

Structure avec 1 champ: 0,46 s délégué "Unsafe" .70 délégué "Safe" s 4.5 de FieldInfo .SetValue

Structure avec 4 champs: 0,46 s "Unsafe" délégué FieldInfo.SetValue de 4.5 délégué "Safe" s 0,88

Notez que la boxe fait le th La vitesse de la version «sûre» diminue avec la taille de la structure, alors que les deux autres méthodes ne sont pas affectées par la taille de la structure. Je suppose qu'à un certain moment, le coût de la boxe dépasserait le coût de la réflexion. Mais je ne ferais pas confiance à la version "Unsafe" dans une capacité importante.

+0

Bonne réponse. Pour mon problème particulier (c'est-à-dire pour implémenter ma bibliothèque http://fasterflect.codeplex.com), je ne voulais pas du tout utiliser 'ref'. Au lieu de cela, j'ai besoin que le code appelant passe dans un wrapper struct qui est un type de valeur. –

+0

Il ne me laissera pas commenter la réponse de Hugh ci-dessous, alors je vais répondre ici. Fondamentalement, pour définir un champ sur une structure, vous devez d'abord encadrer la structure, comme ceci: object x = new MyStruct(); Ensuite, vous passez l'objet en boîte par réf. Si vous savez déjà de quel type vous disposez à l'avance, vous n'avez pas du tout besoin de ce code. Et si vous ne savez pas de quel type il s'agit, cela signifie qu'il est déjà dans une référence d'objet. J'espère que cela a du sens. –

+0

@BryceWagner peut-il être fait avec le paramètre connu à l'exécution? Est-ce que changer 'typeof (objet) .MakeByRefType(), typeof (objet)' à 'typeof (knownType) .MakeByRefType(), typeof (knownFieldType)' est suffisant? Je ne connais pas la partie émettrice et je ne peux toujours pas utiliser le .net 4.0. – AgentFire

4

Après quelques expériences:

public delegate void ClassFieldSetter<in T, in TValue>(T target, TValue value) where T : class; 

public delegate void StructFieldSetter<T, in TValue>(ref T target, TValue value) where T : struct; 

public static class FieldSetterCreator 
{ 
    public static ClassFieldSetter<T, TValue> CreateClassFieldSetter<T, TValue>(FieldInfo field) 
     where T : class 
    { 
     return CreateSetter<T, TValue, ClassFieldSetter<T, TValue>>(field); 
    } 

    public static StructFieldSetter<T, TValue> CreateStructFieldSetter<T, TValue>(FieldInfo field) 
     where T : struct 
    { 
     return CreateSetter<T, TValue, StructFieldSetter<T, TValue>>(field); 
    } 

    private static TDelegate CreateSetter<T, TValue, TDelegate>(FieldInfo field) 
    { 
     return (TDelegate)(object)CreateSetter(field, typeof(T), typeof(TValue), typeof(TDelegate)); 
    } 

    private static Delegate CreateSetter(FieldInfo field, Type instanceType, Type valueType, Type delegateType) 
    { 
     if (!field.DeclaringType.IsAssignableFrom(instanceType)) 
      throw new ArgumentException("The field is declared it different type"); 
     if (!field.FieldType.IsAssignableFrom(valueType)) 
      throw new ArgumentException("The field type is not assignable from the value"); 

     var paramType = instanceType.IsValueType ? instanceType.MakeByRefType() : instanceType; 
     var setter = new DynamicMethod("", typeof(void), 
             new[] { paramType, valueType }, 
             field.DeclaringType.Module, true); 

     var generator = setter.GetILGenerator(); 
     generator.Emit(OpCodes.Ldarg_0); 
     generator.Emit(OpCodes.Ldarg_1); 
     generator.Emit(OpCodes.Stfld, field); 
     generator.Emit(OpCodes.Ret); 

     return setter.CreateDelegate(delegateType); 
    } 
} 

La principale différence de l'approche de l'arbre d'expression est que les champs en lecture seule peuvent également être modifiés.

+0

La meilleure réponse! – AgentFire

2

Ce code fonctionne pour struct sans utiliser ref:

private Action<object, object> CreateSetter(FieldInfo field) 
{ 
    var instance = Expression.Parameter(typeof(object)); 
    var value = Expression.Parameter(typeof(object)); 

    var body = 
     Expression.Block(typeof(void), 
      Expression.Assign(
       Expression.Field(
        Expression.Unbox(instance, field.DeclaringType), 
        field), 
       Expression.Convert(value, field.FieldType))); 

    return (Action<object, object>)Expression.Lambda(body, instance, value).Compile(); 
} 

Voici mon code de test:

public struct MockStruct 
{ 
    public int[] Values; 
} 

[TestMethod] 
public void MyTestMethod() 
{ 
    var field = typeof(MockStruct).GetField(nameof(MockStruct.Values)); 
    var setter = CreateSetter(field); 
    object mock = new MockStruct(); //note the boxing here. 
    setter(mock, new[] { 1, 2, 3 }); 
    var result = ((MockStruct)mock).Values; 
    Assert.IsNotNull(result); 
    Assert.IsTrue(new[] { 1, 2, 3 }.SequenceEqual(result)); 
}