快速获取 Expression 方法调用目标的方法
Fast way to get Expression method call target
给出如下代码行,
Expression<Action> expression = () => target.ToString();
是否有快速获取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(); }
但是非常非常慢。
有没有更快的方法获取方法调用表达式的目标?
对我的代码(GetDelegate
、DelegateCompile
和 DelegateDynamicInvoke
)以及@IvanStoev 的代码(GetFunc
、FuncCompile
和 FuncInvoke
) 产生这个结果:
| 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 |
因此,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>();
}
}
我能想到的避免耗时 Compile
操作的唯一方法是使用反射递归地评估表达式内容。
一般地执行此操作(处理所有情况)是一项复杂的任务。目前有 80 多个 ExpressionTypes, all of them with different semantics (well, some fall into categories with corresponding base classes). In order to handle them all, one should probably create custom ExpressionVisitor 并实施评估引擎(可能带有某种评估堆栈)。
换句话说,很多work/code。
但是...如果我们将表达式限制为 2 种类型 - ConstantExpression (constant value) and 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();
}
使用它的方法是
public object GetExpressionTarget<T>(Expression<T> expression)
{
var methodCall = (MethodCallExpression)expression.Body;
return Evaluate(methodCall.Object);
}
我没有性能比较结果,但即使这是使用反射,它应该比使用反射 和 动态 IL 代码发出的 Compile
快得多,不计算 DynamicMethod
的创建和调用它的委托。
随着时间的推移,我对 Ivan 的解决方案进行了一些扩展,以防它对其他人有所帮助
static object Evaluate(
Expression expression
)
{
switch (expression)
{
case ConstantExpression e:
return e.Value;
case MemberExpression e when e.Member is FieldInfo field:
return field.GetValue(
Evaluate(
e.Expression
)
);
case MemberExpression e when e.Member is PropertyInfo property:
return property.GetValue(
Evaluate(
e.Expression
)
);
case ListInitExpression e when e.NewExpression.Arguments.Count() == 0:
var collection = e.NewExpression.Constructor.Invoke(new object[0]);
foreach(var i in e.Initializers)
{
i.AddMethod.Invoke(
collection,
i.Arguments
.Select(
a => Evaluate(a)
)
.ToArray()
);
}
return collection;
case MethodCallExpression e:
return e.Method.Invoke(
Evaluate(e.Object),
e.Arguments
.Select(
a => Evaluate(a)
)
.ToArray()
);
default:
//TODO: better messaging
throw new NotSupportedException();
}
}
给出如下代码行,
Expression<Action> expression = () => target.ToString();
是否有快速获取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(); }
但是非常非常慢。
有没有更快的方法获取方法调用表达式的目标?
对我的代码(GetDelegate
、DelegateCompile
和 DelegateDynamicInvoke
)以及@IvanStoev 的代码(GetFunc
、FuncCompile
和 FuncInvoke
) 产生这个结果:
| 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 |
因此,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>();
}
}
我能想到的避免耗时 Compile
操作的唯一方法是使用反射递归地评估表达式内容。
一般地执行此操作(处理所有情况)是一项复杂的任务。目前有 80 多个 ExpressionTypes, all of them with different semantics (well, some fall into categories with corresponding base classes). In order to handle them all, one should probably create custom ExpressionVisitor 并实施评估引擎(可能带有某种评估堆栈)。
换句话说,很多work/code。
但是...如果我们将表达式限制为 2 种类型 - ConstantExpression (constant value) and 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();
}
使用它的方法是
public object GetExpressionTarget<T>(Expression<T> expression)
{
var methodCall = (MethodCallExpression)expression.Body;
return Evaluate(methodCall.Object);
}
我没有性能比较结果,但即使这是使用反射,它应该比使用反射 和 动态 IL 代码发出的 Compile
快得多,不计算 DynamicMethod
的创建和调用它的委托。
随着时间的推移,我对 Ivan 的解决方案进行了一些扩展,以防它对其他人有所帮助
static object Evaluate(
Expression expression
)
{
switch (expression)
{
case ConstantExpression e:
return e.Value;
case MemberExpression e when e.Member is FieldInfo field:
return field.GetValue(
Evaluate(
e.Expression
)
);
case MemberExpression e when e.Member is PropertyInfo property:
return property.GetValue(
Evaluate(
e.Expression
)
);
case ListInitExpression e when e.NewExpression.Arguments.Count() == 0:
var collection = e.NewExpression.Constructor.Invoke(new object[0]);
foreach(var i in e.Initializers)
{
i.AddMethod.Invoke(
collection,
i.Arguments
.Select(
a => Evaluate(a)
)
.ToArray()
);
}
return collection;
case MethodCallExpression e:
return e.Method.Invoke(
Evaluate(e.Object),
e.Arguments
.Select(
a => Evaluate(a)
)
.ToArray()
);
default:
//TODO: better messaging
throw new NotSupportedException();
}
}