【问题标题】:Using Expressions to access struct properties in C#在 C# 中使用表达式访问结构属性
【发布时间】:2016-04-24 04:42:24
【问题描述】:

我一直在使用以下代码来缓存属性 getter/setter 委托,以便快速访问该功能:

class PropertyHelper
{
    public static Func<object, object> BuildGetter(PropertyInfo propertyInfo)
    {
        var method = propertyInfo.GetGetMethod(true);

        var obj = Expression.Parameter(typeof(object), "o");
        Expression<Func<object, object>> expr =
                Expression.Lambda<Func<object, object>>(
                        Expression.Convert(
                                Expression.Call(
                                        Expression.Convert(obj, method.DeclaringType),
                                        method),
                                typeof(object)),
                        obj);
        return expr.Compile();
    }

    public static Action<object, object> BuildSetter(PropertyInfo propertyInfo)
    {
        var method = propertyInfo.GetSetMethod(true);

        var obj = Expression.Parameter(typeof(object), "o");
        var value = Expression.Parameter(typeof(object));

        Expression<Action<object, object>> expr =
            Expression.Lambda<Action<object, object>>(
                Expression.Call(
                    Expression.Convert(obj, method.DeclaringType),
                    method,
                    Expression.Convert(value, method.GetParameters()[0].ParameterType)),
                obj,
                value);

        Action<object, object> action = expr.Compile();
        return action;
    }
}

这在访问类对象的属性时效果很好,但是当我将它用于结构对象时它会失败。例如,考虑以下代码:

public struct LocationStruct
{
    public double X { get; set; }
    public double Y { get; set; }
}

public class LocationClass
{
    public double X { get; set; }
    public double Y { get; set; }
}

public class Tester
{
    public static void TestSetX()
    {
        Type locationClassType = typeof(LocationClass);
        PropertyInfo xProperty = locationClassType.GetProperty("X");
        Action<object, object> setter = PropertyHelper.BuildSetter(xProperty);

        LocationStruct testLocationClass = new LocationClass();
        setter(testLocationClass, 10.0);
        if (testLocationClass.X == 10.0)
        {
            MessageBox.Show("Worked for the class!");
        }


        Type locationStructType = typeof(LocationStruct);
        xProperty = locationStructType.GetProperty("X");
        setter = PropertyHelper.BuildSetter(xProperty);

        LocationStruct testLocationStruct = new LocationStruct();
        setter(testLocationStruct, 10.0);
        if (testLocationStruct.X != 10.0)
        {
            MessageBox.Show("Didn't work for the struct!");
        }
    }
}

第一部分起作用,将 testLocationClass 的 X 值设置为 10。但是,由于 LocationStruct 是一个结构,testLocationStruct 是按值传递的,该值(在委托调用的方法内部)将其 X 设置为10,但上面代码块中的testLocationStruct对象保持不变。

因此,我需要一种访问结构对象属性的方法,类似于上述方法(仅适用于类对象的属性)。我尝试使用“按引用传递”模式来完成此操作,但我无法让它发挥作用。

谁能提供类似的 BuildGetter 和 BuildSetter 方法来缓存结构属性值的 getter/setter 委托?

【问题讨论】:

  • 快速说明:这些不称为 lambda 表达式,只是表达式或表达式树。 Lambda 更多地指的是闭包,即 C# 中的匿名函数。
  • Rgr...感谢您的注意。我将更改我的标题和标签。
  • 请详细说明这对值类型(结构)不起作用。您是否遇到处理盒装值类型的问题?如果是这样,是否可以通过更改代码使其通用而不是假设 System.Object 来解决它?您应该发布代码,演示如何将您的实现用于值类型,清楚地表明这对您不起作用。
  • 彼得,感谢您的评论。我添加了更多细节和示例。

标签: c# struct linq-expressions propertyinfo


【解决方案1】:

您需要注意两件事才能使其正常工作:

  1. 在创建setter表达式树时,值类型需要使用Expression.Unbox,引用类型需要使用Expression.Convert
  2. 使用值类型调用 setter 时,您需要确保它被装箱以使用指向结构的指针设置值(而不是处理结构的副本)。

新的实现看起来像这样(只显示了新的 setter 和 test 方法,因为其余的都是一样的):

public static Action<object, object> BuildSetter(PropertyInfo propertyInfo)
{
    // Note that we are testing whether this is a value type
    bool isValueType = propertyInfo.DeclaringType.IsValueType;
    var method = propertyInfo.GetSetMethod(true);
    var obj = Expression.Parameter(typeof (object), "o");
    var value = Expression.Parameter(typeof (object));

    // Note that we are using Expression.Unbox for value types
    // and Expression.Convert for reference types
    Expression<Action<object, object>> expr = 
        Expression.Lambda<Action<object, object>>(
            Expression.Call(
                isValueType ? 
                    Expression.Unbox(obj, method.DeclaringType) :
                    Expression.Convert(obj, method.DeclaringType), 
                method, 
                Expression.Convert(value, method.GetParameters()[0].ParameterType)), 
                obj, value);
    Action<object, object> action = expr.Compile();
    return action;
}

以及调用已编译setter的代码:

...
Type locationStructType = typeof (LocationStruct);
xProperty = locationStructType.GetProperty("X");
setter = PropertyHelper.BuildSetter(xProperty);
LocationStruct testLocationStruct = new LocationStruct();

// Note the boxing of the struct before calling the setter
object boxedStruct = testLocationStruct;
setter(boxedStruct, 10.0);
testLocationStruct = (LocationStruct)boxedStruct;
...

打印出来:

Worked for the class!
Worked for the struct!

我还准备了一个 .Net fiddle,在这里展示了工作实现:https://dotnetfiddle.net/E6WZmK

有关Expression.Unbox 步骤的说明,请参阅此答案:https://stackoverflow.com/a/32158735/521773

【讨论】:

  • 这是一个很棒的答案。谢谢!
【解决方案2】:

结构作为参数是按值传递的,而 ref/out 似乎不适用于表达式,您可以考虑使用返回结构实例的新函数签名:

static Func<MethodInfo, object, object, object> s1 = (MethodInfo set, object instance, object val) =>
{
    set.Invoke(instance, new object[] { val });
    return instance;
};

// Non-Generic approach
static Func<object, object, object> BuildSetter5(PropertyInfo propertyInfo)
{
    var method = propertyInfo.GetSetMethod(true);

    var obj = Expression.Parameter(typeof(object), "o");
    var value = Expression.Parameter(typeof(object));

    Expression<Func<object, object, object>> expr =
        Expression.Lambda<Func<object, object, object>>(
            Expression.Call(
                s1.Method,
                Expression.Constant(method),
                obj,
                Expression.Convert(value, method.GetParameters()[0].ParameterType)),
            obj,
            value);

    Func<object, object, object> action = expr.Compile();
    return action;
}

// Generic approach
static Func<T, object, T> BuildSetter6<T>(PropertyInfo propertyInfo) where T : struct
{
    var method = propertyInfo.GetSetMethod(true);

    var obj = Expression.Parameter(typeof(T), "o");
    var value = Expression.Parameter(typeof(object));

    Expression<Func<T, object, T>> expr =
        Expression.Lambda<Func<T, object, T>>(
            Expression.Convert(
                Expression.Call(
                    s1.Method,
                    Expression.Constant(method),
                    Expression.Convert(obj, typeof(object)),
                    Expression.Convert(value, method.GetParameters()[0].ParameterType)),
                typeof(T)),
            obj,
            value);

    Func<T, object, T> action = expr.Compile();
    return action;
}

【讨论】:

  • 感谢您的回复。我将您的代码复制粘贴到我的测试器类中,但是当我使用上面代码中的 xProperty 并调用您的 BuildSetter5(xProperty) 时,它在尝试定义 expr 时遇到异常。异常说: System.Double 类型的表达式不能用于 System.Object<.cctor>b__0(System.Reflection.MethodInfo, System.Object, System.Object) 方法的 System.Object 类型的参数。我想我可以解决这个问题,但是——也许更重要的是——如果我必须用调用的结果替换原始结构,那么该方法对我不起作用。
猜你喜欢
  • 2014-04-25
  • 1970-01-01
  • 1970-01-01
  • 2020-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-15
  • 1970-01-01
相关资源
最近更新 更多