2009-08-13 8 views
11

Diciamo che ho il seguente codice che aggiorna un campo di un struct usando il riflesso. Poiché l'istanza della struct viene copiata nel metodo DynamicUpdate, it needs to be boxed to an object before being passed.Generare il metodo dinamico per impostare un campo di una struct invece di utilizzare la riflessione

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); 
    } 
} 

Il codice funziona correttamente. Ora, diciamo che non voglio usare il riflesso perché è lento. Invece, voglio generare alcuni CIL direttamente modificando il campo id e convertire tale CIL in un delegato riutilizzabile (ad esempio, utilizzando la funzione Metodo dinamico). Specialmente, voglio sostituire il codice precedente con s/t in questo modo:

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 
}  

La mia domanda: esiste un modo per implementare CreateSetIdDelegate stralci da utilizzando uno dei seguenti tecniche?

  1. Genera CIL che richiama il setter utilizzando reflection (come il primo segmento di codice in questo post). Questo non ha senso, dato che il requisito è quello di sbarazzarsi della riflessione, ma è una possibile implementazione, quindi cito.
  2. Invece di utilizzare Action<object, object>, utilizzare un delegato personalizzato la cui firma è public delegate void Setter(ref object target, object value).
  3. Invece di utilizzare Action<object, object>, utilizzare Action<object[], object> con il primo elemento dell'array come oggetto di destinazione.

La ragione per cui non mi piace 2 & 3 è perché io non voglio avere i delegati diversi per il setter di oggetto e setter di struct (così come non volendo fare il set-oggetto-campo delegare più complicato del necessario, ad es. Action<object, object>). Suppongo che l'implementazione di CreateSetIdDelegate genererebbe CIL diversi a seconda che il tipo di destinazione sia struct o object, ma voglio che restituisca lo stesso delegato che offre la stessa API all'utente.

+2

sta usando una struct mutabile * veramente * la scelta migliore qui? È quasi sempre un dolore per molte ragioni, e sembra che tu stia correndo in alcune di esse ... –

+1

Hai considerato di compilare un albero di espressioni invece di emettere IL? Dovrebbe essere molto più facile –

+0

@Jon: in realtà sto costruendo un'API di riflessione veloce (http://fasterflect.codeplex.com/) in modo che il supporto per le operazioni di struct reflection sia auspicabile da alcune persone. –

risposta

14

EDIT di nuovo: Questo funziona ora.

C'è un modo fantastico per farlo in C# 4, ma prima dovrai scrivere il tuo codice di emissione ILGenerator. Hanno aggiunto un ExpressionType.Assign a .NET Framework 4.

Questo funziona in C# 4 (testato):

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: Qui è stato il mio codice di prova.

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

Buon uso dell'affermazione di C# 4 ET. +1. Ma l'uso di * ref * è s/t che voglio evitare (vedi il secondo punto nella mia domanda) perché non voglio creare delegati separati per struct setter e class setter. –

+5

Per una struttura, devi passare per rif per apportare modifiche all'originale. Dato che non puoi aggiungere 'ref' o' out' ai delegati di 'Action <>', hai le seguenti opzioni: 1) Usa una classe invece di struct, 2) Usa delegati personalizzati, o 3) Engineer in modo che tu non è necessario apportare modifiche alla struct e passare la struct per il valore [boxed]. :) –

1

Si consiglia di dare un'occhiata a metodi dinamici (riflessione non deve essere lento!) ...

Gerhard ha un bel post su questo: http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/

+0

Questo è quello che sto facendo (e chiedendo in questa domanda); Voglio sostituire la riflessione utilizzando il metodo dinamico e la domanda chiede se posso condividere la stessa API di creazione dei delegati per entrambe le strutture e le classi. –

10

mi sono imbattuto in un problema simile, e ci ho messo più di un fine settimana, ma alla fine ho pensato che dopo un sacco di ricerca, lettura e smontaggio progetti di test C#. E questa versione richiede solo .NET 2, non 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)); 
} 

Notare il contenuto di "# UNSAFE_IL".In realtà ho inventato 2 modi per farlo, ma il primo è davvero ... hackish. Per citare da Ecma-335, il documento degli standard per IL:

"A differenza della casella, che è necessaria per creare una copia di un tipo di valore da utilizzare nell'oggetto, unbox non è necessario per copiare il tipo di valore dall'oggetto In genere calcola semplicemente l'indirizzo del tipo di valore che è già presente all'interno dell'oggetto in scatola. "

Quindi, se si desidera giocare pericolosamente, è possibile utilizzare OpCodes.Unbox per modificare l'handle dell'oggetto in un puntatore alla struttura, che può quindi essere utilizzato come primo parametro di un Stfld o Callvirt. In questo modo, in effetti, si finisce per modificare la struttura in posizione e non è nemmeno necessario passare l'oggetto di destinazione per riferimento.

Tuttavia, si noti che lo standard non garantisce che Unbox fornirà un puntatore alla versione in scatola. In particolare, suggerisce che Nullable <> può causare a Unbox di creare una copia. Ad ogni modo, se ciò accade, probabilmente otterrai un errore silenzioso, in cui imposta il campo o il valore della proprietà su una copia locale che viene immediatamente scartata.

Quindi il modo sicuro per farlo è passare il tuo oggetto per riferimento, memorizzare l'indirizzo in una variabile locale, apportare la modifica, quindi reinserire il risultato e reinserirlo nel parametro oggetto ByRef.

Ho fatto alcuni tempi di massima, chiamando ogni versione 10.000.000 volte, con 2 diverse strutture:

Struttura con 1 campo: .46 s "Unsafe" delegato .70 s delegato "Safe" 4,5 s FieldInfo .SetValue

Struttura con 4 campi: .46 s "Unsafe" delegato 0,88 s delegato "Safe" 4,5 s FieldInfo.SetValue

Si noti che la boxe fa il tH La velocità della versione "sicura" diminuisce con la dimensione della struttura, mentre gli altri due metodi non sono influenzati dalle dimensioni della struttura. Suppongo che a un certo punto il costo della boxe supererebbe il costo di riflessione. Ma non mi fiderei della versione "Unsafe" in qualsiasi capacità importante.

+0

Bella risposta. Per il mio particolare problema (ad esempio per implementare la mia libreria http://fasterflect.codeplex.com), non volevo assolutamente utilizzare "ref". Invece ho bisogno che il codice chiamante passi in qualche wrapper di struct che è un tipo di valore. –

+0

Non mi consentirà di commentare la risposta di Hugh in basso, quindi risponderò qui. Fondamentalmente per impostare un campo su una struct, devi prima inserire la struct, in questo modo: object x = new MyStruct(); Quindi si passa l'oggetto in scatola in di rif. Se sai già che tipo hai a che fare con il tempo, non hai affatto bisogno di questo codice. E se non sai di che tipo si tratta, significa che probabilmente è già in riferimento a un oggetto. Spero che abbia un senso. –

+0

@BryceWagner può essere fatto con un parametro noto in fase di esecuzione? Cambierà 'typeof (object) .MakeByRefType(), typeof (object)' a 'typeof (knownType) .MakeByRefType(), typeof (knownFieldType)' sarà sufficiente? Non conosco la parte che emette e non posso ancora usare .net 4.0. – AgentFire

4

Dopo alcuni esperimenti:

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 differenza rispetto all'approccio albero di espressione è che i campi di sola lettura possono anche essere modificati.

+0

La più grande risposta! – AgentFire

2

Questo codice funziona per le strutture senza l'utilizzo di 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(); 
} 

Qui è il mio codice di prova:

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)); 
}