Как "отменить кавычки" при создании дерева выражений из лямбды?
Давайте предположим, что у меня есть какая-то функция 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." );