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

Posted

技术标签:

【中文标题】如何在 C# 表达式树中设置字段值?【英文标题】:How do I set a field value in an C# Expression tree? 【发布时间】:2010-09-24 05:07:03 【问题描述】:

给定:

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”?

【问题讨论】:

【参考方案1】:

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

【讨论】:

很好的回应巴里,你回答了我最初的问题。我将发布另一个问题,我需要先调用转换的操作码......谢谢! 只是好奇,这种方法与仅使用 System.Reflection 和 MemberInfos 设置属性有什么区别? 我对您的回答 +1 感到非常惊讶。干得好!恭喜:) 非常有用的东西,但要小心极端情况!此 MakeSetter 方法不处理值类型的情况。 @Oleg Mihailik:你说不处理值类型是什么意思?如果该字段是值类型,则没有问题。如果源 (T) 是值类型,我认为没有任何 setter 可以真正按预期工作。【参考方案2】:

如前所述,设置字段是有问题的。您可以(在 3.5 中)使用单个方法,例如属性设置器 - 但只能是间接的。正如 here 所讨论的,这在 4.0 中变得更加容易。但是,如果您确实有属性(而不是字段),您可以简单地使用 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);
    

【讨论】:

我很想听听为什么它被否决了……对我来说似乎是一个相当不错的旁观者;仅适用于属性,但不需要 Reflection.Emit 或 Expression... 在给定的链接中,提到了Expression.AssignProperty。那是什么? @nawful 在 4.0 树中更加丰富,允许完整的代码块。然而,大多数 linq 提供程序和编译器都不允许您这样做。我假设它是通过属性分配的;) @MarcGravell 你能否正确地标记我的名字,以便我收到警报。哦,好的,但是你从哪里找到这个 Expression.AssignProperty 的?一些文档或链接? @nawfal 抱歉打错了;如果您阅读该帖子,它会说它基于 4.0 CTP - 即 .NET 4.0 的预发布版本;大概这在 RTM 版本中更改为 .Assign。【参考方案3】:
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 来说问题更大)。 啊,没关系。只需在DynamicMethod 构造函数中设置restrictedSkipVisibility【参考方案4】:

实际上,在 .NET 3.5 中有一种方法可以使用表达式树设置属性和字段。它可能是一些不支持Delegate.CreateDelegate 的 PCL 配置文件的唯一选择(除了 Reflection.Emit):

对于字段,诀窍是将字段作为 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);
    

【讨论】:

【参考方案5】:

我曾经做过这门课。也许有帮助:

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【参考方案6】:

为了完整起见,这里是 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();
        
    

【讨论】:

以上是关于如何在 C# 表达式树中设置字段值?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 中创建表达式树来表示“String.Contains("term")”?

递归 lambda 表达式在 C# 中遍历树

C# 表达式树的重要意义

表达式树中的 C# 4“动态”

C# 表达式树分页扩展

如何在 iReport 中设置 Excel 单元格格式