快速获取表达式方法调用目标的方法

Tho*_*kow 1 c# performance delegates expression dynamic-invoke

鉴于以下代码行,

Expression<Action> expression = () => target.ToString();
Run Code Online (Sandbox Code Playgroud)

有没有快速获取target对象的方法?

下面的代码有效

public object GetExpressionTarget<T>(Expression<T> expression)
{
    MethodCallExpression methodCall = (MethodCallExpression) expression.Body;
    LambdaExpression theTarget = Expression.Lambda(methodCall.Object, null);
    Delegate compiled = theTarget.Compile();

    return compiled.DynamicInvoke();    }
Run Code Online (Sandbox Code Playgroud)

但是非常非常慢。


有没有更快的方法来获取方法调用表达式的目标?


对我的代码 ( GetDelegate,DelegateCompileDelegateDynamicInvoke) 以及@IvanStoev 的代码 ( GetFunc,FuncCompileFuncInvoke) 进行基准测试会产生以下结果:

|                Method |           Mean |         Error |        StdDev |
|---------------------- |----------------|---------------|---------------|
|       DelegateCompile | 165,068.477 ns | 2,671.3001 ns | 2,498.7358 ns |
|           FuncCompile | 160,956.199 ns | 2,133.5343 ns | 1,995.7093 ns |
| DelegateDynamicInvoke |   1,148.191 ns |    11.7213 ns |    10.9642 ns |
|            FuncInvoke |       3.040 ns |     0.0264 ns |     0.0247 ns |
Run Code Online (Sandbox Code Playgroud)

所以,Invoke实际上比 快得多DynamicInvoke,但瓶颈实际上是Compile调用。有没有办法在target不编译表达式的情况下获取对象?

基准代码:

public class Program
{
    private Delegate @delegate;
    private Func<object> func;

    private static Delegate GetDelegate(Expression<Action> expression)
    {
        MethodCallExpression methodCall = (MethodCallExpression) expression.Body;
        return Expression.Lambda(methodCall.Object, null).Compile();
    }

    private static Func<object> GetFunc(Expression<Action> expression)
    {
        MethodCallExpression methodCall = (MethodCallExpression) expression.Body;
        return Expression.Lambda<Func<object>>(methodCall.Object).Compile();
    }

    [GlobalSetup]
    public void Setup()
    {
        object o = new object();
        Expression<Action> expression = () => o.ToString();

        this.@delegate = Program.GetDelegate(expression);
        this.func = Program.GetFunc(expression);
    }

    [Benchmark]
    public void DelegateCompile()
    {
        object o = new object();
        Expression<Action> expression = () => o.ToString();

        Program.GetDelegate(expression);
    }

    [Benchmark]
    public void FuncCompile()
    {
        object o = new object();
        Expression<Action> expression = () => o.ToString();

        Program.GetFunc(expression);
    }

    [Benchmark]
    public void DelegateDynamicInvoke()
    {
        this.@delegate.DynamicInvoke();
    }

    [Benchmark]
    public void FuncInvoke()
    {
        this.func.Invoke();
    }

    public static void Main(string[] args)
    {
        BenchmarkRunner.Run<Program>();
    }
}
Run Code Online (Sandbox Code Playgroud)

Iva*_*oev 6

我能想到的避免耗时Compile操作的唯一方法是使用反射递归地评估表达式内容。

一般地执行此操作(处理所有情况)是一项复杂的任务。目前有超过 80 个ExpressionType,它们都有不同的语义(嗯,有些属于具有相应基类的类别)。为了处理所有这些,可能应该创建自定义ExpressionVisitor并实现评估引擎(可能带有某种评估堆栈)。

换句话说,很多工作/代码。

但是...如果我们将表达式限制为 2 种类型 - ConstantExpression(常量值)和MemberExpression(常量值的字段或属性),那么有一个相对简单的解决方案。所讨论的方法已经包含关于传递Expression<Action>和样本表达式目标(这是一个闭包)属于常量值字段类别的假设。

主要工作是在私有递归方法中完成的,如下所示:

static object Evaluate(Expression expression)
{
    if (expression == null)
        return null;
    if (expression is ConstantExpression constExpression)
        return constExpression.Value;
    if (expression is MemberExpression memberExpression)
    {
        var target = Evaluate(memberExpression.Expression);
        if (memberExpression.Member is FieldInfo field)
            return field.GetValue(target);
        if (memberExpression.Member is PropertyInfo property)
            return property.GetValue(target);
    }
    throw new NotSupportedException();
}
Run Code Online (Sandbox Code Playgroud)

使用它的方法是

public object GetExpressionTarget<T>(Expression<T> expression)
{
    var methodCall = (MethodCallExpression)expression.Body;
    return Evaluate(methodCall.Object);
}
Run Code Online (Sandbox Code Playgroud)

我没有性能比较结果,但即使这是使用反射,它也应该比Compile使用反射动态 IL 代码发出的要快得多,不包括创建 aDynamicMethod和委托来调用它。