从PropertyInfo获取访问器作为Func<object>和Action<object>委托

7

我需要通过反射在运行时调用属性,并且这些属性的调用频率很高。因此,我正在寻找性能最佳的解决方案,这意味着我可能会避免使用反射。我想将属性访问器存储为“Func”和“Action”委托存储在一个列表中,然后调用它们。

private readonly Dictionary<string, Tuple<Func<object>, Action<object>>> dataProperties =
        new Dictionary<string, Tuple<Func<object>, Action<object>>>();

private void BuildDataProperties()
{
    foreach (var keyValuePair in this.GetType()
        .GetProperties(BindingFlags.Instance | BindingFlags.Public)
        .Where(p => p.Name.StartsWith("Data"))
        .Select(
            p =>
                new KeyValuePair<string, Tuple<Func<object>, Action<object>>>(
                    p.Name,
                    Tuple.Create(this.GetGetter(p), this.GetSetter(p)))))
    {
        this.dataProperties.Add(keyValuePair.Key, keyValuePair.Value);
    }
}

现在的问题是,我如何获取访问器委托作为Func和Action委托以供后续调用?一个天真的实现仍然使用反射进行调用,看起来像这样:
private Func<object> GetGetter(PropertyInfo info)
{
    // 'this' is the owner of the property
    return () => info.GetValue(this);
}

private Action<object> GetSetter(PropertyInfo info)
{
    // 'this' is the owner of the property
    return v => info.SetValue(this, v);
}

我该如何在不使用反射的情况下实现上述方法?表达式是否是最快的方法?我尝试过像这样使用表达式:
private Func<object> GetGetter(PropertyInfo info)
{
    // 'this' is the owner of the property
    return
        Expression.Lambda<Func<object>>(
            Expression.Convert(Expression.Call(Expression.Constant(this), info.GetGetMethod()), typeof(object)))
            .Compile();
}

private Action<object> GetSetter(PropertyInfo info)
{
    // 'this' is the owner of the property
    var method = info.GetSetMethod();
    var parameterType = method.GetParameters().First().ParameterType;
    var parameter = Expression.Parameter(parameterType, "value");
    var methodCall = Expression.Call(Expression.Constant(this), method, parameter);

    // ArgumentException: ParameterExpression of type 'System.Boolean' cannot be used for delegate parameter of type 'System.Object'
    return Expression.Lambda<Action<object>>(methodCall, parameter).Compile();
}

但是,如果属性的类型不恰好为System.Object,则在GetSetter的最后一行会出现以下异常:

ArgumentException: ParameterExpression of type 'System.Boolean' cannot be used for delegate parameter of type 'System.Object'


2
只需使用反射并缓存找到的委托,使用delegate.CreateDelegate。 - James Barrass
你是否必须使用 Func<object>Action<object> 而不是适当的类型,例如 Func<bool>Action<bool> - Jon Skeet
所以我不太明白表达式的部分,也许是我理解有误,但听起来好像你已经有了答案——它与我在下面发布的答案相匹配。 - Jamiec
@JonSkeet 我已经更新了我的问题,解释了为什么我认为需要使用Func<object>Action<object>而不是适当的类型。 - bitbonk
@bitbonk 我已经编辑了我的回答。 - Sinan AKYAZICI
显示剩余5条评论
3个回答

5
这是我的方式,它表现良好。但我不知道它的性能如何。
    public static Func<object, object> GenerateGetterFunc(this PropertyInfo pi)
    {
        //p=> ((pi.DeclaringType)p).<pi>

        var expParamPo = Expression.Parameter(typeof(object), "p");
        var expParamPc = Expression.Convert(expParamPo,pi.DeclaringType);

        var expMma = Expression.MakeMemberAccess(
                expParamPc
                , pi
            );

        var expMmac = Expression.Convert(expMma, typeof(object));

        var exp = Expression.Lambda<Func<object, object>>(expMmac, expParamPo);

        return exp.Compile();
    }

    public static Action<object, object> GenerateSetterAction(this PropertyInfo pi)
    {
        //p=> ((pi.DeclaringType)p).<pi>=(pi.PropertyType)v

        var expParamPo = Expression.Parameter(typeof(object), "p");
        var expParamPc = Expression.Convert(expParamPo,pi.DeclaringType);

        var expParamV = Expression.Parameter(typeof(object), "v");
        var expParamVc = Expression.Convert(expParamV, pi.PropertyType);

        var expMma = Expression.Call(
                expParamPc
                , pi.GetSetMethod()
                , expParamVc
            );

        var exp = Expression.Lambda<Action<object, object>>(expMma, expParamPo, expParamV);

        return exp.Compile();
    }

2
我认为您需要做的是以正确的类型返回Lambda,使用object作为参数,但在调用setter之前,在表达式中进行正确类型的转换:
 private Action<object> GetSetter(PropertyInfo info)
 {
     // 'this' is the owner of the property
     var method = info.GetSetMethod();
     var parameterType = method.GetParameters().First().ParameterType;

     // have the parameter itself be of type "object"
     var parameter = Expression.Parameter(typeof(object), "value");

     // but convert to the correct type before calling the setter
     var methodCall = Expression.Call(Expression.Constant(this), method, 
                        Expression.Convert(parameter,parameterType));

     return Expression.Lambda<Action<object>>(methodCall, parameter).Compile();

  }

Live example: http://rextester.com/HWVX33724


1
我在调查这个问题的过程中学到了很多。感谢楼主! - Jamiec
成功了!我尝试使用 Expression.Convert 的方式和你之前一样,只是 typeof(object)parameterType 位置颠倒了。 - bitbonk

1
你需要使用像 Convert.ChangeType 这样的转换方法。属性类型是 bool。但 GetSetter 方法的返回类型是 object。因此,你应该将表达式中的属性类型从 bool 转换为 object。
    public static Action<T, object> GetSetter<T>(T obj, string propertyName)
    {
        ParameterExpression targetExpr = Expression.Parameter(obj.GetType(), "Target");
        MemberExpression propExpr = Expression.Property(targetExpr, propertyName);
        ParameterExpression valueExpr = Expression.Parameter(typeof(object), "value");
        MethodCallExpression convertExpr = Expression.Call(typeof(Convert), "ChangeType", null, valueExpr, Expression.Constant(propExpr.Type));
        UnaryExpression valueCast = Expression.Convert(convertExpr, propExpr.Type);
        BinaryExpression assignExpr = Expression.Assign(propExpr, valueCast);
        return Expression.Lambda<Action<T, object>>(assignExpr, targetExpr, valueExpr).Compile();
    }

    private static Func<T, object> GetGetter<T>(T obj, string propertyName)
    {
        ParameterExpression arg = Expression.Parameter(obj.GetType(), "x");
        MemberExpression expression = Expression.Property(arg, propertyName);
        UnaryExpression conversion = Expression.Convert(expression, typeof(object));
        return Expression.Lambda<Func<T, object>>(conversion, arg).Compile();
    }

实时演示

编辑:

public class Foo
{
    #region Fields

    private readonly Dictionary<string, Tuple<Func<Foo, object>, Action<Foo, object>>> dataProperties = new Dictionary<string, Tuple<Func<Foo, object>, Action<Foo, object>>>();

    #endregion

    #region Properties

    public string Name { get; set; }
    public string Data1 { get; set; }
    public string Data2 { get; set; }
    public string Data3 { get; set; }
    public int ID { get; set; }

    #endregion

    #region Methods: public

    public void BuildDataProperties()
    {
        foreach (
            var keyValuePair in
                GetType()
                    .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                    .Where(p => p.Name.StartsWith("Data"))
                    .Select(p => new KeyValuePair<string, Tuple<Func<Foo, object>, Action<Foo, object>>>(p.Name, Tuple.Create(GetGetter(this, p.Name), GetSetter(this, p.Name))))) {
                        dataProperties.Add(keyValuePair.Key, keyValuePair.Value);
                    }
    }

    #endregion

    #region Methods: private

    private Func<T, object> GetGetter<T>(T obj, string propertyName)
    {
        ParameterExpression arg = Expression.Parameter(obj.GetType(), "x");
        MemberExpression expression = Expression.Property(arg, propertyName);
        UnaryExpression conversion = Expression.Convert(expression, typeof(object));
        return Expression.Lambda<Func<T, object>>(conversion, arg).Compile();
    }

    private Action<T, object> GetSetter<T>(T obj, string propertyName)
    {
        ParameterExpression targetExpr = Expression.Parameter(obj.GetType(), "Target");
        MemberExpression propExpr = Expression.Property(targetExpr, propertyName);
        ParameterExpression valueExpr = Expression.Parameter(typeof(object), "value");
        MethodCallExpression convertExpr = Expression.Call(typeof(Convert), "ChangeType", null, valueExpr, Expression.Constant(propExpr.Type));
        UnaryExpression valueCast = Expression.Convert(convertExpr, propExpr.Type);
        BinaryExpression assignExpr = Expression.Assign(propExpr, valueCast);
        return Expression.Lambda<Action<T, object>>(assignExpr, targetExpr, valueExpr).Compile();
    }

    #endregion
}

您可以像下面这样从字典中获取值:

        var t = new Foo { ID = 1, Name = "Bla", Data1 = "dadsa"};
        t.BuildDataProperties();
        var value = t.dataProperties.First().Value.Item1(t);

现场演示


看起来不错,除了我不想要转换功能而是想要强制转换功能。在你的演示中,即使属性是int类型,s(t, true);也可以正常工作。它会将true静默转换为(int)1。它不会抛出InvalidCastException异常(在我的情况下应该抛出)。 - bitbonk
你是对的。Change.Convert 对你不起作用。所以你可以像 Convert.ChangeType 一样制作你自己的更改方法,并在此使用它。我不确定如何在此中安全地进行类型强制转换,也许你可以提出一个新的问题来询问。 - Sinan AKYAZICI

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