2009-08-13 13 views
11

Załóżmy, że mam następujący kod, który aktualizuje pole przy użyciu odbicia. Ponieważ instancja struct jest kopiowana do metody DynamicUpdate, it needs to be boxed to an object before being passed.Generowanie dynamicznej metody ustawiania pola struktury struct zamiast używania refleksji

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

Kod działa poprawnie. Teraz powiedzmy, że nie chcę korzystać z refleksji, ponieważ jest powolna. Zamiast tego chcę wygenerować CIL bezpośrednio modyfikując pole id i przekonwertować CIL na delegata wielokrotnego użytku (powiedzmy, używając funkcji dynamicznej metody). Specjalnie, chcę zastąpić powyższy kod z S/T to tak:

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 
}  

Moje pytanie: jest jakiś sposób, aby wdrożyć CreateSetIdDelegate wyjątki od od użyciu jednej z następujących technik?

  1. Wygeneruj CIL, który wywołuje setera za pomocą odbicia (jako pierwszy segment kodu w tym wpisie). To nie ma sensu, biorąc pod uwagę, że trzeba pozbyć się refleksji, ale jest to możliwa implementacja, więc wspomnę tylko.
  2. Zamiast używać Action<object, object>, użyj niestandardowego przedstawiciela, którego podpis to public delegate void Setter(ref object target, object value).
  3. Zamiast używać Action<object, object>, należy użyć Action<object[], object>, a pierwszym elementem tablicy będzie obiekt docelowy.

Powodem nie lubię 2 & 3 jest dlatego, że nie chcą mieć różne delegatów dla seter seter obiektu oraz od struktury (jak również nie chcąc, aby nastawioną-Object-field delegować bardziej skomplikowane niż to konieczne, np. Action<object, object>). Sądzę, że implementacja CreateSetIdDelegate wygenerowałaby różne CIL zależnie od tego, czy typem docelowym jest struktura czy obiekt, ale chcę, aby zwrócił tego samego delegata oferującego użytkownikowi ten sam interfejs API.

+2

stosuje zmienny struct * naprawdę * Twój najlepszą opcją tutaj? To prawie zawsze ból z wielu powodów i wydaje się, że wpadłeś na niektóre z nich ... –

+1

Czy rozważałeś skompilowanie drzewa wyrażeń zamiast emitowania IL? Powinno być znacznie łatwiej. –

+0

@Jon: faktycznie buduję API szybkiego refleksji (http://fasterflect.codeplex.com/), więc wsparcie dla struktur refleksyjnych byłoby pożądane przez niektórych ludzi. –

Odpowiedz

14

ponownie EDIT: To teraz działa.

Istnieje cudowny sposób, aby to zrobić w C# 4, ale będziesz musiał napisać swój własny kod emitujący ILGenerator dla czegokolwiek wcześniej. Dodali ExpressionType.Assign do .NET Framework 4.

ta działa w C# 4 (testowane):

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: Tu był mój kodu testu.

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

Przyjemne użycie instrukcji C# 4 ET. +1. Ale użycie * ref * to s/t, którego chcę uniknąć (patrz drugi punkt w moim pytaniu), ponieważ nie chcę tworzyć oddzielnych delegatów dla selektora struct i setera klasy. –

+5

Dla struct, musisz przekazać go przez referencję, aby dokonać zmian w oryginale. Ponieważ nie możesz dodać 'ref' lub' out' do delegatów 'Action <>, masz następujące opcje: 1) Użyj klasy zamiast struct, 2) Użyj niestandardowych delegatów lub 3) Inżynier, dzięki czemu nie trzeba wprowadzać zmian w strukturze i przekazywać struct przez wartość [boxed]. :) –

1

Możesz rzucić okiem na metody dynamiczne (odbicie nie musi być wolna!) ...

Gerhard ma ładny post o które: http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/

+0

To właśnie robię (i pytam w tym pytaniu); Chcę zastąpić odbicie za pomocą metody dynamicznej, a pytanie pyta, czy mogę współużytkować ten sam interfejs API do tworzenia delegatów dla obu klas i klas. –

10

wpadłem na podobny problem, i zajęło mi prawie cały weekend, ale w końcu zorientowaliśmy się po wielu poszukiwaniach, czytania i demontażu projektów testowych C#. Ta wersja wymaga tylko .NET 2, a nie 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)); 
} 

Uwaga na rzeczy "#if UNSAFE_IL".Tak naprawdę wymyśliłem 2 sposoby na zrobienie tego, ale pierwszy jest naprawdę ... hackish. Aby zacytować z Ecma-335, dokument norm dla IL:

"W przeciwieństwie do pola, które jest wymagane do wykonania kopii typu wartości do użycia w obiekcie, unbox nie jest wymagane do skopiowania typu wartości z obiektu Zazwyczaj po prostu oblicza adres typu wartości, który jest już obecny wewnątrz obiektu w ramce. "

Jeśli więc chcesz zagrać niebezpiecznie, możesz użyć OpCodes.Unbox, aby zmienić uchwyt obiektu na wskaźnik do swojej struktury, który może być następnie użyty jako pierwszy parametr Stfld lub Callvirt. Robiąc to w ten sposób, w rzeczywistości modyfikuje się strukturę, a ty nie musisz nawet przekazywać obiektu docelowego przez ref.

Należy jednak pamiętać, że standard nie gwarantuje, że Unbox wyświetli wskaźnik do wersji pudełkowej. W szczególności sugeruje, że Nullable <> może spowodować, że Unbox utworzy kopię. W każdym razie, jeśli tak się stanie, prawdopodobnie wystąpi cicha awaria, gdy ustawia wartość pola lub właściwości na lokalnej kopii, która jest natychmiast odrzucana.

Tak więc bezpiecznym sposobem jest przekazanie obiektu przez referencję, zapisanie adresu w zmiennej lokalnej, dokonanie modyfikacji, a następnie ponowne zapakowanie wyniku i umieszczenie go z powrotem w parametrze obiektu ByRef.

Zrobiłem kilka szorstkich czasy, nazywając Każda wersja 10.000.000 razy z 2 różnych strukturach:

Struktura z 1 pola: .46 S "niebezpieczne" delegatów .70 S "Safe" delegat 4,5 s FieldInfo .SetValue

struktura z 4 dziedzin: .46 s "niebezpieczne" delegatów .88 s "Safe" delegat 4,5 s FieldInfo.SetValue

Zauważ, że boks sprawia tH e "Bezpieczna" wersja zmniejsza prędkość z rozmiarem struktury, podczas gdy pozostałe dwie metody nie mają wpływu na rozmiar struktury. Sądzę, że w pewnym momencie koszt boksu przekroczyłby koszt odbicia. Ale nie ufałbym wersji "Unsafe" w żadnej ważnej roli.

+0

Dobra odpowiedź. W moim konkretnym problemie (tj. Zaimplementowaniu mojej biblioteki http://fasterflect.codeplex.com), nie chciałem w ogóle korzystać z "ref". Zamiast tego wymagam podania kodu wywołującego w jakimś strukturalnym opakowaniu, które jest typem wartości. –

+0

Nie pozwolę sobie skomentować odpowiedzi Hugh poniżej, więc odpowiem tutaj. Zasadniczo, aby ustawić pole na strukturze, musisz najpierw umieścić ramkę w strukturze, na przykład: object x = new MyStruct(); Następnie przekazujesz obiekt w pudełku według ref. Jeśli już wiesz, z jakim typem masz do czynienia przed czasem, w ogóle nie potrzebujesz tego kodu. A jeśli nie wiesz, jaki to jest, oznacza to, że prawdopodobnie znajduje się już w odnośniku do obiektu. Mam nadzieję, że ma to sens. –

+0

@BryceWagner można to zrobić za pomocą parametru znanego w czasie wykonywania? Czy zmiana 'typeof (object) .MakeByRefType(), typeof (object)' na 'typeof (knownType) .MakeByRefType(), typeof (knownFieldType)' wystarczy? Po prostu nie znam części emitującej i nadal nie mogę korzystać z .net 4.0. – AgentFire

4

Po kilku eksperymentach:

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

Główną różnicą od podejścia drzewa wyrażenie jest, że pola readonly można również zmienić.

+0

Największa odpowiedź! – AgentFire

2

Ten kod działa na elemencie bez użycia 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(); 
} 

Tu jest mój kodu testu:

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)); 
} 
Powiązane problemy