Подтвердить что ты не робот

Генерировать динамический метод для установки поля структуры вместо использования отражения

Скажем, у меня есть следующий код, который обновляет поле struct с использованием отражения. Поскольку экземпляр struct скопирован в метод DynamicUpdate, он должен быть помещен в бокс для объекта перед передачей.

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

Код работает нормально. Теперь, скажем, я не хочу использовать рефлексию, потому что она медленная. Вместо этого я хочу сгенерировать некоторый CIL, напрямую изменяющий поле id и преобразовывая этот CIL в многократно используемый делегат (скажем, используя функцию Dynamic Method). В частности, я хочу заменить указанный выше код на s/t следующим образом:

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
}    

Мой вопрос: есть ли способ реализовать исключения CreateSetIdDelegate из одного из следующих методов?

  • Сгенерировать CIL, который вызывает сеттер с использованием отражения (как 1-й сегмент кода в этом сообщении). Это не имеет смысла, поскольку требование состоит в том, чтобы избавиться от рефлексии, но это возможная реализация, поэтому я просто упоминаю.
  • Вместо использования Action<object, object> используйте пользовательский делегат, подпись которого public delegate void Setter(ref object target, object value).
  • Вместо Action<object, object> используйте Action<object[], object>, когда 1-й элемент массива является целевым объектом.

Причина, по которой мне не нравятся 2 и 3, состоит в том, что я не хочу иметь разных делегатов для установщика объекта и сеттера структуры (а также не хочу, чтобы делегировать делегат объекта set-object чем необходимо, например, Action<object, object>). Я полагаю, что реализация CreateSetIdDelegate создавала бы другой CIL в зависимости от того, является ли целевой тип структурой или объектом, но я хочу, чтобы он возвращал тому же делегату, предлагающему пользователю тот же API.

4b9b3361

Ответ 1

РЕДАКТИРОВАТЬ снова. Теперь это создает структуру.

Там великолепный способ сделать это на С# 4, но вам придется написать свой собственный код ILGenerator emit для чего-либо до этого. Они добавили ExpressionType.Assign в .NET Framework 4.

Это работает в С# 4 (проверено):

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

Изменить: Вот мой тестовый код.

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

Ответ 2

Я столкнулся с подобной проблемой, и мне потребовалось большую часть выходных, но я, наконец, понял это после многого поиска, чтения и демонстрации проектов тестирования С#. И для этой версии требуется только .NET 2, а не 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));
}

Обратите внимание на материал #if UNSAFE_IL. Я на самом деле придумал два способа сделать это, но первый из них действительно... хакерский. Чтобы привести цитату из Ecma-335, документ стандартов для IL:

"В отличие от поля, которое требуется для создания копии типа значения для использования в объекте, unbox не требуется для копирования типа значения из объекта. Обычно он просто вычисляет адрес типа значения, который уже присутствовать внутри объекта в штучной упаковке."

Итак, если вы хотите играть опасно, вы можете использовать OpCodes.Unbox для изменения дескриптора объекта в указатель на вашу структуру, который затем можно использовать в качестве первого параметра Stfld или Callvirt. Выполнение этого способа фактически завершает модификацию структуры на месте, и вам даже не нужно передавать целевой объект по ссылке.

Однако обратите внимание, что стандарт не гарантирует, что Unbox даст вам указатель на коробку. В частности, это говорит о том, что Nullable < > может вызвать Unbox для создания копии. В любом случае, если это произойдет, вы, вероятно, получите молчаливый сбой, когда он устанавливает значение поля или свойства в локальной копии, которая затем сразу же отбрасывается.

Таким образом, безопасный способ сделать это - передать свой объект по ref, сохранить адрес в локальной переменной, внести изменения, а затем снова добавить результат и поместить его обратно в свой параметр объекта ByRef.

Я сделал некоторые грубые тайминги, назвав каждую версию 10 000 000 раз, с двумя разными структурами:

Структура с 1 полем: .46 s "Небезопасный" делегат .70 s "Безопасный" делегат 4.5 s FieldInfo.SetValue

Структура с 4 полями: .46 s "Небезопасный" делегат .88 s "Безопасный" делегат. 4.5 s FieldInfo.SetValue

Обратите внимание, что бокс делает скорость "безопасной" версии уменьшенной с размером структуры, тогда как другие два метода не зависят от размера структуры. Думаю, в какой-то момент стоимость бокса будет превышать стоимость отражения. Но я не буду доверять "Небезопасной" версии в любой важной емкости.

Ответ 3

После некоторых экспериментов:

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

Основное отличие от дерева дерева выражений состоит в том, что поля readonly также могут быть изменены.

Ответ 4

Этот код работает для структур без использования ссылки:

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

Вот мой тестовый код:

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