Могу ли я установить свойство структуры с помощью выражений?

У меня есть следующий метод, который устанавливает значение для данного PropertyInfo на данный TInstance, Это чтобы избежать неэффективности рефлексии.

public static Action<TInstance, object> CreateSetter<TInstance>(PropertyInfo propertyInfo, bool includeNonPublic = false)
{
    var setMethod = propertyInfo.GetSetMethod(includeNonPublic);

    var instance = Expression.Parameter(typeof(TInstance), "instance");
    var value = Expression.Parameter(typeof(object), "value");
    var valueCast = !propertyInfo.PropertyType.IsValueType
        ? Expression.TypeAs(value, propertyInfo.PropertyType)
        : Expression.Convert(value, propertyInfo.PropertyType);

    return Expression.Lambda<Action<TInstance, object>>(
        Expression.Call(instance, setMethod, valueCast), instance, value).Compile();
}

Итак, учитывая следующую модель:

public sealed class PersonClass
{
    public string Name {get; set;}    
}

Я могу установить Name с помощью:

var person = new PersonClass(); 
var nameProp = person.GetType().GetProperties().Where(p => p.Name == "Name").First();
var nameSetter = CreateSetter<PersonClass>(nameProp);
nameSetter(person, "Foo");

Это все хорошо, однако, если я попробую метод с struct например:

public struct PersonStruct
{
    public string Name {get; set;}    
}

Имя всегда null, Я подозреваю, что бокс / распаковка кусает меня как-то.

На самом деле, если я использую FastMember такое же поведение проявляется при использовании:

PersonStruct person = new PersonStruct();   
var accessor = TypeAccessor.Create(person.GetType());       
accessor[person, "Name"] = "Foo";

Однако, когда я бокс person как object затем FastMember может правильно установить значение:

object person = new PersonStruct(); 
var accessor = TypeAccessor.Create(person.GetType());       
accessor[person, "Name"] = "Foo";

Любые идеи, как я могу справиться с этим боксом внутри CreateSetter когда TInstance такое тип значения?

2 ответа

Решение

Как отмечено в комментариях, вы действительно не должны создавать изменяемые структуры. Однако, чтобы ответить на вопрос, структуры являются типами значений, и поэтому копия вашей структуры передается в Action и, следовательно, первоначальная личная ценность не изменяется.

Вам нужен способ пройти struct по ссылке. Однако выражения не поддерживают создание "методов", которые принимают параметры по ссылке.

Что вы можете сделать, это использовать DynamicMethod Класс, чтобы сделать что-то вроде этого:

public delegate void StructSetter<TInstance>(ref TInstance instance, object value) where TInstance : struct;

public StructSetter<TInstance> CreateSetter<TInstance>(
    PropertyInfo propertyInfo,
    bool includeNonPublic = false) where TInstance : struct
{
    DynamicMethod method =
        new DynamicMethod(
            "Set",
            typeof(void),
            new [] { typeof(TInstance).MakeByRefType(), typeof(object )},
            this.GetType());

    var generator = method.GetILGenerator();

    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Ldarg_1);
    generator.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod(includeNonPublic));
    generator.Emit(OpCodes.Ret);

    return (StructSetter<TInstance>)method.CreateDelegate(typeof (StructSetter<TInstance> ));
}

Мы должны были создать StructSetter делегировать, потому что стандарт Action делегаты не поддерживают передачу по ссылке.

Не забудьте кэшировать делегат, иначе затраты на компиляцию замедляют работу вашего приложения.

Вам нужно выражение, которое создает делегат, который принимает аргумент by-ref, чтобы он влиял на передаваемую структуру, а не на копию. Например:

public struct PersonStruct
{
    public string Name {get; set;}    
}

delegate void FirstByRefAction<T1, T2>(ref T1 arg1, T2 arg2);

void Main()
{
    ParameterExpression par1 = Expression.Parameter(typeof(PersonStruct).MakeByRefType());
    ParameterExpression par2 = Expression.Parameter(typeof(string));
    FirstByRefAction<PersonStruct, string> setter = Expression.Lambda<FirstByRefAction<PersonStruct, string>>(
        Expression.Assign(Expression.Property(par1, "Name"), par2),
        par1, par2
        ).Compile();
    PersonStruct testStruct = new PersonStruct();
    setter(ref testStruct, "Test Name");
    Console.Write(testStruct.Name); // outputs "Test Name"
}

Это чтобы избежать неэффективности рефлексии.

Обратите внимание, что типы выражений, вызывающих методы и свойства, в основном используют внутреннее отражение. В случае интерпретированных выражений они делают это при каждом вызове, в случае скомпилированных выражений IL отражение все еще используется на этапе компиляции.

Другие вопросы по тегам