如何获取使用局部变量的ConstantExpression的值

本文关键字:局部变量 ConstantExpression 的值 何获取 获取 | 更新日期: 2023-09-27 18:05:13

我创建了一个覆盖VisitConstant的ExpressionVisitor实现。但是,当我创建一个使用局部变量的表达式时,我似乎无法获得该变量的实际值。

public class Person
{
  public string FirstName { get; set; }
}
string name = "Michael";
Expression<Func<Person, object>> exp = p => p.FirstName == name;

我如何从ConstantExpression中获得变量name的值?我唯一能想到的是:

string fieldValue = value.GetType().GetFields().First().GetValue(value).ToString();

显然这不是很灵活....

一个稍微复杂一点的例子是:

Person localPerson = new Person { FirstName = "Michael" };
Expression<Func<Person, object>> exp = p => p.FirstName == localPerson.FirstName;

如何获取使用局部变量的ConstantExpression的值

编辑:好的,感谢AHM的评论,现在你的意思更清楚了。

基本上,代码被编译为在一个单独的类中捕获name,然后应用字段访问从引用它的实例的常量表达式中获取它的值。(它必须这样做,因为您可以在创建表达式后更改name 的值-但是表达式捕获变量,而不是值。)

所以你实际上不想在VisitConstantConstantExpression上做任何事情-你想在VisitMember的字段访问上工作。您需要从ConstantExpression子进程获取值,然后将该值赋给FieldInfo以获取值:

using System;
using System.Linq.Expressions;
using System.Reflection;
public class Person
{
    public string FirstName { get; set; }
}
static class Program
{
    static void Main(string[] args)
    {
        string name = "Michael";
        Expression<Func<Person, object>> exp = p => p.FirstName == name;
        new Visitor().Visit(exp);
    }
}
class Visitor : ExpressionVisitor    
{
    protected override Expression VisitMember
        (MemberExpression member)
    {
        if (member.Expression is ConstantExpression &&
            member.Member is FieldInfo)
        {
            object container = 
                ((ConstantExpression)member.Expression).Value;
            object value = ((FieldInfo)member.Member).GetValue(container);
            Console.WriteLine("Got value: {0}", value);
        }
        return base.VisitMember(member);
    }
}

编辑:好的,稍微复杂一点的访客类版本:

class Visitor : ExpressionVisitor    
{
    protected override Expression VisitMember
        (MemberExpression memberExpression)
    {
        // Recurse down to see if we can simplify...
        var expression = Visit(memberExpression.Expression);
        // If we've ended up with a constant, and it's a property or a field,
        // we can simplify ourselves to a constant
        if (expression is ConstantExpression)
        {
            object container = ((ConstantExpression) expression).Value;
            var member = memberExpression.Member;
            if (member is FieldInfo)
            {
                object value = ((FieldInfo)member).GetValue(container);
                return Expression.Constant(value);
            }
            if (member is PropertyInfo)
            {
                object value = ((PropertyInfo)member).GetValue(container, null);
                return Expression.Constant(value);
            }
        }
        return base.VisitMember(memberExpression);
    }
}

现在运行:

var localPerson = new Person { FirstName = "Jon" };
Expression<Func<Person, object>> exp = p => p.FirstName == localPerson.FirstName;
Console.WriteLine("Before: {0}", exp);
Console.WriteLine("After: {0}", new Visitor().Visit(exp));

给出结果:

Before: p => Convert((p.FirstName == 
           value(Program+<>c__DisplayClass1).localPerson.FirstName))
After: p => Convert((p.FirstName == "Jon"))

对于您列出的两种情况,我是这样解决的:

基本上假设右侧的'=='可以被视为不带参数并返回值的函数,它可以被编译为c#委托并调用以检索该值,而不必担心右侧的代码究竟是做什么的。

基本示例代码在

下面
class Visitor : ExpressionVisitor {
  protected override Expression VisitBinary( BinaryExpression node ) {
    var memberLeft = node.Left as MemberExpression;
    if ( memberLeft != null && memberLeft.Expression is ParameterExpression ) {
      var f = Expression.Lambda( node.Right ).Compile();
      var value = f.DynamicInvoke();
      }
    return base.VisitBinary( node );
    }
  }

查找查找"arg"的二进制操作符。member == something"然后只编译/计算右边,对于两个示例,您提供的结果都是字符串"Michael"。

注意,如果你的右手边使用了像

这样的lambda参数,这就失败了

p。FirstName == CallSomeFunc(p.FirstName)

一般来说,你需要实现自己的ExpressionVisitor与覆盖的VisitConstant和VisitMember,我们还需要一个堆栈的MemberAccess节点。

  • 中的VisitMember将节点放到堆栈
  • 在VisitConstant中创建一个'while循环'来分析前一个节点是否为MemberExpression:
    • 获取前一个节点的Member属性
    • 检测是否为FieldInfo或PropertyInfo
    • 调用字段/属性信息的GetValue -它将是您需要的常量值或中间成员的值,可以用于在复杂情况下获得下一个值(见下文)
    • 从栈中删除MemberExpression
    • 闭环

在以下情况下需要循环

var a = new { new b { c = true; }  }
var expression = () => a.b.c;

这是访问常量方法

的一部分
    protected override Expression VisitConstant(ConstantExpression node)
    {
                    MemberExpression prevNode;
                    var val = node.Value;
                    while ((prevNode = PreviousNode as MemberExpression) != null)
                    {
                        var fieldInfo = prevNode.Member as FieldInfo;
                        var propertyInfo = prevNode.Member as PropertyInfo;
                        if (fieldInfo != null)
                            val = fieldInfo.GetValue(val);
                        if (propertyInfo != null)
                            val = propertyInfo.GetValue(val);
                        Nodes.Pop();
                    }
                    // we got the value
                    // now val = constant we was looking for
        return node;
    }

PreviousNode是做Stack的属性。Peek

ConstantExpression的问题是编译器put's使用私有匿名类的对象来存储lambda关闭的值,因此常量的值是该私有类的对象的值。要访问"实际的"常量,你必须分析 ConstantExpression之前出现的表达式。一个过于简化的解决方案可能看起来像这样:

<>之前 public sealed class ConstantValueExtractor : ExpressionVisitor { public static object ExtractFirstConstant(Expression expression) { var visitor = new ConstantValueExtractor(); visitor.Visit(expression); return visitor.ConstantValue; } private ConstantValueExtractor() { } private object ConstantValue { get; set; } #region ExpressionVisitor Members public override Expression Visit(Expression node) { this.pathToValue.Push(node); var result = base.Visit(node); this.pathToValue.Pop(); return result; } protected override Expression VisitConstant(ConstantExpression node) { // The first expression in the path is a ConstantExpression node itself, so just skip it. var parentExpression = this.pathToValue.FirstOrDefault( expression => expression.NodeType == ExpressionType.MemberAccess); if (parentExpression != null) { // You might get notable performance overhead here, so consider caching // compiled lambda or use other to extract the value. var valueProviderExpression = Expression.Lambda>( Expression.Convert(parentExpression, typeof(object))); var valueProvider = valueProviderExpression.Compile(); this.ConstantValue = valueProvider(); } return base.VisitConstant(node); } #endregion #region private fields private Stack pathToValue = new Stack(); #endregion } class Test { static void Main() { string name = "Michael"; Expression> exp = p => p.FirstName == name; var value = ConstantValueExtractor.ExtractFirstConstant(exp); Console.WriteLine(value); } } 之前

我怀疑它是否能处理足够复杂的表达式,但你应该知道它是如何做到的。

好的,这看起来很有趣。显然,正在发生的事情是c#将本地stackframe作为常量对象传递给表达式的参数。如果你在你得到的表达式上面添加另一个表达式,比如fx.:

var count = 18;
Expression<Func<Person, object>> expr2 = p => p.FirstName == name && count > 10;

那么你的方法将停止工作- "name"字段将不再是奇怪的"local-variables"对象的第一个字段。

我不知道表达式的行为是这样的,但似乎你必须寻找在MemberExpression与常量表达式作为它的内部表达式。然后,您可以通过计算表达式来获得该值:

protected override Expression VisitMember(MemberExpression node) {
    if (node.Expression.NodeType == ExpressionType.Constant) {
        var inner = (ConstantExpression)node.Expression;
        var value = (node.Member as FieldInfo).GetValue(inner.Value);
    }
    return base.VisitMember(node);
}

我不知道这有多可靠,您可能需要更深入地检查成员表达式,但在这里展示的简化示例中,上面的方法可以工作。

这里是另一个没有DynamicInvoke的解决方案因为树是递归遍历的,所以不需要堆栈。如果访问常量或静态方法的成员,则在遍历子节点后计算该值。

替换像

这样的表达式
var myFoo = new Foo{ OtherObjects = new List<OtherObjec>{ new OtherObject {Prop = 5 }};
var expression = () => myFoo.OtherObjects[0].Prop
--->
var expression = () => 5

这个方法是不完整的,并不能处理所有复杂的情况,但它可以是一个很好的开始,为其他寻找解决这个问题的方法

public class ReplaceConstantVisitor : ExpressionVisitor
{
    private int m_memberAccessDepth = 0;
    private object m_constantValue;
    private bool m_constantOnStack = false;
    public ReplaceConstantVisitor()
    {
        m_constantValue = null;
    }
    protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
    {
        var result = base.VisitMethodCall(methodCallExpression);
        if (m_constantOnStack)
        {
            var reducedArguments = methodCallExpression.Arguments.OfType<ConstantExpression>().ToList();
            if (reducedArguments.Count() == methodCallExpression.Arguments.Count)
            {
                m_constantValue = methodCallExpression.Method.Invoke(m_constantValue, reducedArguments.Select(x => x.Value).ToArray());
            }
            else
            {
                m_constantOnStack = false;
            }
        }
        return result;
    }
    
    protected override Expression VisitMember(MemberExpression memberExpression)
    {
        m_memberAccessDepth++;
        var result = base.VisitMember(memberExpression);
        m_memberAccessDepth--;
        // initial condition to do replacement
        switch (memberExpression.Expression)
        {
            // replace constant member access
            case ConstantExpression constantExpression:
                m_constantOnStack = true;
                m_constantValue = constantExpression.Value;
                break;
            // replace static member access
            case null when memberExpression.Member.IsStatic():
                m_constantOnStack = true;
                break;
        }
        if (m_constantOnStack)
        {
            switch (memberExpression.Member)
            {
                case PropertyInfo propertyInfo:
                    m_constantValue = propertyInfo.GetValue(m_constantValue);
                    break;
                case FieldInfo fieldInfo:
                    m_constantValue = fieldInfo.GetValue(m_constantValue);
                    break;
                default:
                    m_constantOnStack = false; // error case abort replacement
                    break;
            }
        }
        if (m_constantOnStack && m_memberAccessDepth == 0)
        {
            m_constantOnStack = false;
            var constant = m_constantValue;
            m_constantValue = null;
            return Expression.Constant(constant);
        }
        return result;
    }
}