Как "отменить кавычки" при создании дерева выражений из лямбды?

Давайте предположим, что у меня есть какая-то функция c это возвращение Expression:

Func<int, Expression<Func<int>>> c = (int a) => () => a + 3;

Теперь я хочу создать еще один Expression, но при его создании я бы хотел вызвать функцию c и вставьте его результат как часть нового выражения:

Expression<Func<int>> d = () => 2 + c(3);

Я не могу сделать это, потому что это будет интерпретировать c(3) как вызов функции для преобразования в выражение, и я получу ошибку, которую я не могу добавить int а также Expression<Func<int>>

мне бы хотелось d иметь значение:

(Expression<Func<int>>)( () => 2 + 3 + 3 )

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

Как бы вы сделали это в C#?

В качестве альтернативы, как бы вы сделали это на любом другом языке CLR, который я мог бы использовать в своем проекте на C# с как можно меньшими хлопотами?


Более сложные примеры:

Func<int, Expression<Func<int>>> c = (int a) => () => a*(a + 3);
Expression<Func<int, int>> d = (x) => 2 + c(3 + x);

3+x следует вычислять только один раз в результирующем выражении, даже если оно встречается в теле c в двух местах.


У меня есть сильное чувство, что это не может быть достигнуто в C#, потому что назначение лямбда Expression выполняется компилятором и является своего рода временем компиляции const выражение буквальное. Это было бы сродни созданию компилятора, который понимает обычный строковый литерал "test" понять строковый литерал шаблона "test ${a+b} other" и компилятор C# еще не находится на этой стадии разработки.

Итак, мой главный вопрос на самом деле:

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

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


Кажется, что F# имеет возможность "заключать в кавычки" и "убирать" (склеивать) код:

https://docs.microsoft.com/en-us/dotnet/articles/fsharp/language-reference/code-quotations

3 ответа

Решение

Для обоих ваших примеров это на самом деле можно сделать с двумя посетителями выражений (код прокомментирован):

static class Extensions {
    public static TResult FakeInvoke<TResult>(this Delegate instance, params object[] parameters)
    {
        // this is not intended to be called directly
        throw new NotImplementedException();
    }

    public static TExpression Unwrap<TExpression>(this TExpression exp) where TExpression : Expression {
        return (TExpression) new FakeInvokeVisitor().Visit(exp);
    }

    class FakeInvokeVisitor : ExpressionVisitor {
        protected override Expression VisitMethodCall(MethodCallExpression node) {
            // replace FakeInvoke call
            if (node.Method.Name == "FakeInvoke") {
                // first obtain reference to method being called (so, for c.FakeInvoke(...) that will be "c")
                var func = (Delegate)Expression.Lambda(node.Arguments[0]).Compile().DynamicInvoke();
                // explore method argument names and types
                var argumentNames = new List<string>();
                var dummyArguments = new List<object>();
                foreach (var arg in func.Method.GetParameters()) {
                    argumentNames.Add(arg.Name);
                    // create default value for each argument
                    dummyArguments.Add(arg.ParameterType.IsValueType ? Activator.CreateInstance(arg.ParameterType) : null);
                }
                // now, invoke function with default arguments to obtain expression (for example, this one () => a*(a + 3)).
                // all arguments will have default value (0 in this case), but they are not literal "0" but a reference to "a" member with value 0
                var exp = (Expression) func.DynamicInvoke(dummyArguments.ToArray());
                // this is expressions representing what we passed to FakeInvoke (for example expression (x + 3))
                var argumentExpressions = (NewArrayExpression)node.Arguments[1];
                // now invoke second visitor
                exp = new InnerFakeInvokeVisitor(argumentExpressions, argumentNames.ToArray()).Visit(exp);
                return ((LambdaExpression)exp).Body;
            }
            return base.VisitMethodCall(node);
        }
    }

    class InnerFakeInvokeVisitor : ExpressionVisitor {
        private readonly NewArrayExpression _args;
        private readonly string[] _argumentNames;
        public InnerFakeInvokeVisitor(NewArrayExpression args, string[] argumentNames) {
            _args =  args;
            _argumentNames = argumentNames;
        }
        protected override Expression VisitMember(MemberExpression node) {
            // if that is a reference to one of our arguments (for example, reference to "a")
            if (_argumentNames.Contains(node.Member.Name)) {
                // find related expression
                var idx = Array.IndexOf(_argumentNames, node.Member.Name);
                var argument = _args.Expressions[idx];
                var unary = argument as UnaryExpression;
                // and replace it. So "a" is replaced with expression "x + 3"
                return unary?.Operand ?? argument;
            }
            return base.VisitMember(node);
        }
    }
}

Можно использовать так:

Func<int, Expression<Func<int>>> c = (int a) => () => a * (a + 3);
Expression<Func<int, int>> d = (x) => 2 + c.FakeInvoke<int>(3 + x);
d = d.Unwrap(); // this is now "x => (2 + ((3 + x) * ((3 + x) + 3)))"

Простой случай:

Func<int, Expression<Func<int>>> c = (int a) => () => a + 3;
Expression<Func<int>> d = () => 2 + c.FakeInvoke<int>(3);
d = d.Unwrap(); // this is now "() => 2 + (3 + 3)

С несколькими аргументами:

Func<int, int, Expression<Func<int>>> c = (int a, int b) => () => a * (a + 3) + b;
Expression<Func<int, int>> d = (x) => 2 + c.FakeInvoke<int>(3 + x, x + 5);
d = d.Unwrap(); // "x => (2 + (((3 + x) * ((3 + x) + 3)) + (x + 5)))"

Обратите внимание, что FakeInvoke не является типобезопасным (вы должны явно установить тип возвращаемого значения и аргументы, а не проверять). Но это только для примера, в реальном использовании вы можете создать много перегрузок FakeInvoke, например:

public static TResult FakeInvoke<TArg, TResult>(this Func<TArg, Expression<Func<TResult>>> instance, TArg argument) {
        // this is not intended to be called directly
    throw new NotImplementedException();
}

Приведенный выше код должен быть немного изменен, чтобы правильно обрабатывать такие вызовы (потому что аргументы теперь не в отдельном выражении NewArrayExpression), но это довольно легко сделать. С такими перегрузками вы можете просто сделать:

Expression<Func<int, int>> d = (x) => 2 + c.FakeInvoke(3 + x); // this is type-safe now, you cannot pass non-integer as "3+x", nor you can pass more or less arguments than required.

Случай, когда выражения возвращаются из лямбда-выражения, действительно сложен, потому что эти выражения на самом деле являются закрытыми (public)System.Runtime.CompilerServices.Closure?) объект внутри них, содержащий значения, которые закрывает лямбда. Все это очень затрудняет точную замену формальных параметров фактическими параметрами в дереве выражений.


Вдохновленный ответом Evk, я нашел довольно элегантное решение для более простого случая:

Expression<Func<int, int>> c = (int a) => a * (a + 3);
var d = Extensions.Splice<Func<int, int>>((x) => 2 + c.Embed(3 + x));

// d is now x => (2 + ((3 + x) * ((3 + x) + 3))) expression

public static class Extensions
{
    public static T Embed<T>(this Expression<Func<T>> exp) { throw new Exception("Should not be executed"); }
    public static T Embed<A, T>(this Expression<Func<A, T>> exp, A a) { throw new Exception("Should not be executed"); }
    public static T Embed<A, B, T>(this Expression<Func<A, B, T>> exp, A a, B b) { throw new Exception("Should not be executed"); }
    public static T Embed<A, B, C, T>(this Expression<Func<A, B, C, T>> exp, A a, B b, C c) { throw new Exception("Should not be executed"); }
    public static T Embed<A, B, C, D, T>(this Expression<Func<A, B, C, D, T>> exp, A a, B b, C c) { throw new Exception("Should not be executed"); }

    public static Expression<T> Splice<T>(Expression<T> exp)
    {
        return new SplicingVisitor().Visit(exp) as Expression<T>;
    }
    class SplicingVisitor : ExpressionVisitor
    {
        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.Name == "Embed")
            {
                var mem = node.Arguments[0] as MemberExpression;

                var getterLambda = Expression.Lambda<Func<object>>(mem, new ParameterExpression[0]);
                var lam = getterLambda.Compile().DynamicInvoke() as LambdaExpression;

                var parameterMapping = lam.Parameters.Select((p, index) => new
                {
                    FormalParameter = p,
                    ActualParameter = node.Arguments[index+1]
                }).ToDictionary(o => o.FormalParameter, o => o.ActualParameter);

                return new ParameterReplacerVisitor(parameterMapping).Visit(lam.Body);
            }
            return base.VisitMethodCall(node);
        }
    }
    public class ParameterReplacerVisitor : ExpressionVisitor
    {
        private Dictionary<ParameterExpression, Expression> parameterMapping;
        public ParameterReplacerVisitor(Dictionary<ParameterExpression, Expression> parameterMapping)
        {
            this.parameterMapping = parameterMapping;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if(parameterMapping.ContainsKey(node))
            {
                return parameterMapping[node];
            }
            return base.VisitParameter(node);
        }
    }
}

Используя LinqKit, легко использовать расширяемую оболочку запросов, вызывая AsExpandable() на первый тип сущности. Эта расширяемая оболочка выполняет всю работу, необходимую для создания выражений, чтобы сделать их совместимыми с EF.

Игрушечный пример его использования ниже (Person является EF Code First) -

var ctx = new Test();

Expression<Func<Person, bool>> ageFilter = p => p.Age < 30;

var filtered = ctx.People.AsExpandable()
    .Where(p => ageFilter.Invoke(p) && p.Name.StartsWith("J"));
Console.WriteLine( $"{filtered.Count()} people meet the criteria." );
Другие вопросы по тегам