【问题标题】:How to get the value of a ConstantExpression which uses a local variable?如何获取使用局部变量的 ConstantExpression 的值?
【发布时间】:2011-10-23 08:01:15
【问题描述】:

我创建了一个重写 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;

【问题讨论】:

    标签: c# c#-4.0 expression-trees


    【解决方案1】:

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

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

    因此,您实际上并不想在VisitConstant 中的ConstantExpression 上做任何事情——您想在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"))
    

    【讨论】:

    • 不,在这种情况下,表达式的值属于“UserQuery+c__DisplayClass0”类型,您必须对其进行某种检查才能获得该值。我不知道表达式在传递局部变量时会以这种方式表现 - 也许您可以解释发生了什么?
    • @AHM,表达式可能会在创建它的方法之外被访问,因此所有本地人都可能在此方法之外访问,甚至在调用该方法的对象的生命周期之外,所以他们被转换为此类私人班级的成员。这与您将在 lambda 中看到的行为相同。
    • 这里更好地描述了如何在 C# 中实现闭包:blogs.msdn.com/b/oldnewthing/archive/2006/08/02/686456.aspx
    • 我认为我从未见过 lambda 的这种行为 - 通常 lambda 只包含参数表达式,然后您必须解决这些问题才能评估 lambda。但是你当然是对的,这似乎是表达式树表示闭包的方式。也许我之前从未检查过带有闭包的表达式树:-)
    • 我知道你在这里做什么,但它很快就崩溃了。例如: Person localPerson = new Person { FirstName = "Michael" };然后使用表达式 p => p.FirstName == localPerson.FirstName
    【解决方案2】:

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

    基本上假设您的“==”的右侧可以被视为一个不带参数并返回值的函数,它可以编译为 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”。

    注意,如果您的右手边涉及使用 lamda 参数,例如

    p.FirstName == CallSomeFunc(p.FirstName)

    【讨论】:

    • 不宜使用 Expression.Lambda(node.Right).Compile();它太慢了,DynamicInvoke 也是如此 :( 请看我的回答
    • @SergeiKuz'michev 我同意,但我的回答侧重于灵活性,而不是速度。通过对预期表达式类型的假设,您当然可以比 Compile & DynamicInvoke 做得更好。
    【解决方案3】:

    一般情况下,您需要实现自己的 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 的属性

    【讨论】:

    • 非常喜欢这种做法!
    【解决方案4】:

    ConstantExpression 的问题是编译器 put 使用私有匿名类的对象来存储值 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); } }

    我怀疑它是否适用于足够复杂的表达式,但你应该知道它是如何完成的。

    【讨论】:

    • 好主意 :) 但实施不好,Compile() 很慢,不需要
    • @SerhiiKuzmychov 虽然我不确定覆盖 ConstantExpression 节点是否正确,但对于更复杂的情况确实需要他的方法。请参阅此答案:stackoverflow.com/a/2616980 以获得更正确的实现。这样做的好处是它会逐字评估提供的每个常量表达式。
    • @nawfal 无需使用 Compile() 您已经拥有通过反射获取值所需的一切
    • @SerhiiKuzmychov 我想我不够清楚。我的意思是,虽然 MemeberInfo 方法(例如 Jon Skeet 的方法)适用于给定问题(即评估 local 常量),但如果您想涵盖更多案例,Compile 是最简单/更多的综合方法。例如。考虑以下代码:Person person = new Person { Name = "Michael"); Expression&lt;Func&lt;Person, object&gt;&gt; exp = p =&gt; p.FirstName == person.Name;。您将如何使用 MemberInfo 评估常量 person.Name
    【解决方案5】:

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

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

    然后您的方法将停止工作 - “名称”字段将不再是奇怪的“局部变量”对象中的第一个字段。

    我不知道表达式会以这种方式表现,但您似乎必须使用常量表达式查找 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);
    }
    

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

    【讨论】:

      【解决方案6】:

      这里是另一个没有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;
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2023-03-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多