如何在C#表达式树中设置字段值?

41

给定:

FieldInfo field = <some valid string field on type T>;
ParameterExpression targetExp = Expression.Parameter(typeof(T), "target");
ParameterExpression valueExp = Expression.Parameter(typeof(string), "value");

我该如何编译一个lambda表达式,将"target"参数的字段设置为"value"?

6个回答

77

.Net 4.0:现在有了Expression.Assign,这变得非常容易:

FieldInfo field = typeof(T).GetField("fieldName");
ParameterExpression targetExp = Expression.Parameter(typeof(T), "target");
ParameterExpression valueExp = Expression.Parameter(typeof(string), "value");

// Expression.Property can be used here as well
MemberExpression fieldExp = Expression.Field(targetExp, field);
BinaryExpression assignExp = Expression.Assign(fieldExp, valueExp);

var setter = Expression.Lambda<Action<T, string>>
    (assignExp, targetExp, valueExp).Compile();

setter(subject, "new value");

.Net 3.5:你不能使用它,你必须使用System.Reflection.Emit代替:

class Program
{
    class MyObject
    {
        public int MyField;
    }

    static Action<T,TValue> MakeSetter<T,TValue>(FieldInfo field)
    {
        DynamicMethod m = new DynamicMethod(
            "setter", typeof(void), new Type[] { typeof(T), typeof(TValue) }, typeof(Program));
        ILGenerator cg = m.GetILGenerator();

        // arg0.<field> = arg1
        cg.Emit(OpCodes.Ldarg_0);
        cg.Emit(OpCodes.Ldarg_1);
        cg.Emit(OpCodes.Stfld, field);
        cg.Emit(OpCodes.Ret);

        return (Action<T,TValue>) m.CreateDelegate(typeof(Action<T,TValue>));
    }

    static void Main()
    {
        FieldInfo f = typeof(MyObject).GetField("MyField");

        Action<MyObject,int> setter = MakeSetter<MyObject,int>(f);

        var obj = new MyObject();
        obj.MyField = 10;

        setter(obj, 42);

        Console.WriteLine(obj.MyField);
        Console.ReadLine();
    }
}

1
很好的回答Barry,你解答了我的初步问题。我将发布另一个问题,在那里我需要调用转换的操作码....谢谢! - TheSoftwareJedi
只是好奇,这种方法与仅使用System.Reflection和MemberInfos设置属性有何区别? - chakrit
@Oleg Mihailik:你说值类型没有被处理,是什么意思?如果字段是值类型,那就没有问题。如果源(T)是值类型,我认为没有设置器可以真正按预期工作。 - BladeWise
@BladeWise 是的,没有setter可以工作,因为结构体总是被复制的。不过有一个解决方法 - 通过引用传递。在这里可以找到解决方案:https://dev59.com/QnM_5IYBdhLWcg3wslRW - nawfal
在 .Net 4 版本中,似乎可以跳过获取 FieldInfo 并像这样传递“fieldName”:MemberExpression fieldExp = Expression.Field(targetExp, "fieldName"); - bottlenecked
显示剩余2条评论

24

如已讨论,设置字段存在问题。您可以(在3.5中)间接地使用单个方法,例如属性设置器。有关此更轻松的说明,请参见此处。但是,如果您实际上具有属性(而非字段),则可以通过Delegate.CreateDelegate简单地完成很多工作:

using System;
using System.Reflection;
public class Foo
{
    public int Bar { get; set; }
}
static class Program
{
    static void Main()
    {
        MethodInfo method = typeof(Foo).GetProperty("Bar").GetSetMethod();
        Action<Foo, int> setter = (Action<Foo, int>)
            Delegate.CreateDelegate(typeof(Action<Foo, int>), method);

        Foo foo = new Foo();
        setter(foo, 12);
        Console.WriteLine(foo.Bar);
    }
}

1
我很想知道这个为什么被踩了...在我看来这是一个相当不错的副点;它只适用于属性,但避免了使用Reflection.Emit或Expression的需要... - Marc Gravell
在给定的链接中提到了Expression.AssignProperty,那是什么? - nawfal
在4.0树中,@nawful更加丰富,允许完整的代码块。然而,大多数linq提供程序和编译器都不允许您这样做。我猜它是通过属性分配的 ;) - Marc Gravell
@MarcGravell,您能正确地标记我的名字,这样我就会收到提醒了。哦,好的,但是您在哪里找到这个Expression.AssignProperty的?有什么文档或链接吗? - nawfal
@nawfal 对于笔误不好意思;如果您阅读了那篇文章,它是基于4.0 CTP构建的,即.NET 4.0的预发布版本;可以想象这在RTM版本中已更改为.Assign。 - Marc Gravell

6
private static Action<object, object> CreateSetAccessor(FieldInfo field)
    {
        DynamicMethod setMethod = new DynamicMethod(field.Name, typeof(void), new[] { typeof(object), typeof(object) });
        ILGenerator generator = setMethod.GetILGenerator();
        LocalBuilder local = generator.DeclareLocal(field.DeclaringType);
        generator.Emit(OpCodes.Ldarg_0);
        if (field.DeclaringType.IsValueType)
        {
            generator.Emit(OpCodes.Unbox_Any, field.DeclaringType);
            generator.Emit(OpCodes.Stloc_0, local);
            generator.Emit(OpCodes.Ldloca_S, local);
        }
        else
        {
            generator.Emit(OpCodes.Castclass, field.DeclaringType);
            generator.Emit(OpCodes.Stloc_0, local);
            generator.Emit(OpCodes.Ldloc_0, local);
        }
        generator.Emit(OpCodes.Ldarg_1);
        if (field.FieldType.IsValueType)
        {
            generator.Emit(OpCodes.Unbox_Any, field.FieldType);
        }
        else
        {
            generator.Emit(OpCodes.Castclass, field.FieldType);
        }
        generator.Emit(OpCodes.Stfld, field);
        generator.Emit(OpCodes.Ret);
        return (Action<object, object>)setMethod.CreateDelegate(typeof(Action<object, object>));
    }

似乎 FieldInfo.DeclaringType 必须是公共的才能使其工作。否则,在 .NET 3.5 中会失败并出现 TypeLoadException,在 .NET 4.0/4.5 中会出现 TypeAccessException(尽管 Expression.Assign 在 4.0+ 中可用,因此对于 3.5 来说更成问题)。 - Loathing
啊,没事了。只需要在“DynamicMethod”构造函数中设置“restrictedSkipVisibility”的值即可。 - Loathing

4
实际上,.NET 3.5中有一种使用表达式树设置属性和字段的方法。这可能是一些不支持Delegate.CreateDelegate(除了Reflection.Emit之外)的PCL配置文件的唯一选择:
- 对于字段,技巧是将字段作为ref参数传递,例如:SetField(ref holder.Field, "NewValue"); - 属性(就像Marc已经指出的那样)可以通过反射并调用其setter方法来设置。
以下是提供完整的概念证明的NUnit测试装置。
[TestFixture]
public class CanSetPropAndFieldWithExpressionTreeInNet35
{
    class Holder
    {
        public int Field;
        public string Prop { get; set; }
    }

    public static class FieldAndPropSetter
    {
        public static T SetField<T, TField>(T holder, ref TField field, TField value)
        {
            field = value;
            return holder;
        }

        public static T SetProp<T>(T holder, Action<T> setProp)
        {
            setProp(holder);
            return holder;
        }
    }

    [Test]
    public void Can_set_field_with_expression_tree_in_Net35()
    {
        // Shows how expression could look like:
        Func<Holder, Holder> setHolderField = h => FieldAndPropSetter.SetField(h, ref h.Field, 111);
        var holder = new Holder();
        holder = setHolderField(holder);
        Assert.AreEqual(111, holder.Field);

        var holderType = typeof(Holder);
        var field = holderType.GetField("Field");
        var fieldSetterMethod =
            typeof(FieldAndPropSetter).GetMethod("SetField")
            .MakeGenericMethod(holderType, field.FieldType);

        var holderParamExpr = Expression.Parameter(holderType, "h");
        var fieldAccessExpr = Expression.Field(holderParamExpr, field);

        // Result expression looks like: h => FieldAndPropSetter.SetField(h, ref h.Field, 222)
        var setHolderFieldExpr = Expression.Lambda<Func<Holder, Holder>>(
            Expression.Call(fieldSetterMethod, holderParamExpr, fieldAccessExpr, Expression.Constant(222)),
            holderParamExpr);

        var setHolderFieldGenerated = setHolderFieldExpr.Compile();
        holder = setHolderFieldGenerated(holder);
        Assert.AreEqual(222, holder.Field);
    }

    [Test]
    public void Can_set_property_with_expression_tree_in_Net35()
    {
        // Shows how expression could look like:
        Func<Holder, Holder> setHolderProp = h => FieldAndPropSetter.SetProp(h, _ => _.Prop = "ABC");
        var holder = new Holder();
        holder = setHolderProp(holder);
        Assert.AreEqual("ABC", holder.Prop);

        var holderType = typeof(Holder);
        var prop = holderType.GetProperty("Prop");
        var propSet = prop.GetSetMethod();

        var holderParamExpr = Expression.Parameter(holderType, "h");
        var callSetPropExpr = Expression.Call(holderParamExpr, propSet, Expression.Constant("XXX"));
        var setPropActionExpr = Expression.Lambda(callSetPropExpr, holderParamExpr);

        var propSetterMethod = typeof(FieldAndPropSetter).GetMethod("SetProp").MakeGenericMethod(holderType);

        // Result expression looks like: h => FieldAndPropSetter.SetProp(h, _ => _.Prop = "XXX")
        var setHolderPropExpr = Expression.Lambda<Func<Holder, Holder>>(
            Expression.Call(propSetterMethod, holderParamExpr, setPropActionExpr),
            holderParamExpr);

        var setHolderPropGenerated = setHolderPropExpr.Compile();
        holder = setHolderPropGenerated(holder);
        Assert.AreEqual("XXX", holder.Prop);
    }
}

3

我曾经创建了这个类。或许它能帮到你:

public class GetterSetter<EntityType,propType>
{
    private readonly Func<EntityType, propType> getter;
    private readonly Action<EntityType, propType> setter;
    private readonly string propertyName;
    private readonly Expression<Func<EntityType, propType>> propertyNameExpression;

    public EntityType Entity { get; set; }

    public GetterSetter(EntityType entity, Expression<Func<EntityType, propType>> property_NameExpression)
    {
        Entity = entity;
        propertyName = GetPropertyName(property_NameExpression);
        propertyNameExpression = property_NameExpression;
        //Create Getter
        getter = propertyNameExpression.Compile();
        // Create Setter()
        MethodInfo method = typeof (EntityType).GetProperty(propertyName).GetSetMethod();
        setter = (Action<EntityType, propType>)
                 Delegate.CreateDelegate(typeof(Action<EntityType, propType>), method);
    }


    public propType Value
    {
        get
        {
            return getter(Entity);
        }
        set
        {
            setter(Entity, value);
        }
    }

    protected string GetPropertyName(LambdaExpression _propertyNameExpression)
    {
        var lambda = _propertyNameExpression as LambdaExpression;
        MemberExpression memberExpression;
        if (lambda.Body is UnaryExpression)
        {
            var unaryExpression = lambda.Body as UnaryExpression;
            memberExpression = unaryExpression.Operand as MemberExpression;
        }
        else
        {
            memberExpression = lambda.Body as MemberExpression;
        }
        var propertyInfo = memberExpression.Member as PropertyInfo;
        return propertyInfo.Name;
    }

测试:

var gs = new GetterSetter<OnOffElement,bool>(new OnOffElement(), item => item.IsOn);
        gs.Value = true;
        var result = gs.Value;

不是回答问题,而是关于FieldInfo的内容。 - nawfal

2

仅为完整起见,这里提供getter:

    public static IEnumerable<Func<T, object>> GetTypeGetters<T>()
    {
        var fields = typeof (T).GetFields();

        foreach (var field in fields)
        {
            ParameterExpression targetExp = Expression.Parameter(typeof(T), "target");
            UnaryExpression boxedFieldExp = Expression.Convert(Expression.Field(targetExp, field), typeof(object));
            yield return  Expression.Lambda<Func<T,object>>(boxedFieldExp, targetExp).Compile();
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接