Как получить значение константного выражения, которое использует локальную переменную?

Я создал реализацию ExpressionVisitor, которая переопределяет VisitConstant. Однако, когда я создаю выражение, которое использует локальную переменную, я не могу получить фактическое значение переменной.

public class Person
{
  public string FirstName { get; set; }
}

string name = "Michael";

Expression<Func<Person, object>> exp = p => p.FirstName == name;

Как в мире получить значение переменной "name" из ConstantExpression? Единственное, что я могу придумать, это:

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;

Ответ 1

Вот как я решил это для обоих перечисленных вами случаев.

В принципе, предполагая, что правая сторона вашего '==' может рассматриваться как функция, которая не принимает аргументов и возвращает значение, ее можно скомпилировать в делегат С# и вызвать для получения этого значения, не беспокоясь о том, что именно код с правой стороны.

Итак, базовый пример кода ниже

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", а затем просто компилирует/оценивает правую сторону, и для обоих примеров вы предоставляете результат строкой "Майкл".

Обратите внимание: это не сработает, если ваша правая сторона задействовала аргумент lamda, например

p.FirstName == CallSomeFunc (p.FirstName)

Ответ 2

EDIT: Хорошо, теперь понятно, что вы имеете в виду, благодаря комментарию AHM.

В основном код скомпилирован для захвата name в отдельном классе, а затем применить доступ к полю для получения его значения из выражения константы, которое ссылается на его экземпляр. (Он должен сделать это, так как вы можете изменить значение name после создания выражения - но выражение захватывает переменную, а не значение.)

Итак, вы на самом деле ничего не хотите делать на ConstantExpression в VisitConstant - вы хотите работать с доступом к полю в 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);
    }
}

EDIT: Хорошо, немного более интересная версия класса посетителей:

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 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"))

Ответ 3

В общем, вам нужно реализовать собственный ExpressionVisitor с переопределением VisitConstant и VisitMember, также нам нужен стек для узлов MemberAccess.

  • в VisitMember помещает node в стек
  • в VisitConstant сделать "while loop" для анализа, если предыдущий node MemberExpression:
    • получить свойство Member предыдущего node
    • определить, является ли это полем 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;
    }

ПредыдущийNode - это свойство, которое делает Stack.Peek

Ответ 4

Хорошо, это кажется очень интересным. По-видимому, происходит то, что С# передает локальный стековый фрейм как постоянный объект в качестве параметра вашего выражения. Если вы добавите другое выражение выше того, которое вы получили, например 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);
}

Я не знаю, насколько это надежно, вам может понадобиться более подробно изучить выражение-член, но в приведенном здесь примере simplefied приведенное выше будет работать.

Ответ 5

Проблема с 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);
    }
}

Я сомневаюсь, что он будет работать для достаточно сложных выражений, но вы должны понять, как это можно сделать.