gpt4 book ai didi

c# - 从 lambda 创建表达式树时如何为 'unquote'?

转载 作者:行者123 更新时间:2023-11-30 12:56:30 25 4
gpt4 key购买 nike

假设我有一些函数 c返回 Expression :

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

现在我想创建另一个 Expression , 但在创建过程中我想调用函数 c并将其结果作为新表达式的一部分嵌入:

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

我不能这样做,因为它会解释 c(3)就像要转换为表达式的函数调用,我会得到无法添加的错误 intExpression<Func<int>>

我想要d具有以下值(value):

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

我也有兴趣让它适用于更复杂的表达式,而不仅仅是这个玩具示例。

你会如何用 C# 实现?

或者,您如何使用我可以在我的 C# 项目中使用的任何其他 CLR 语言来尽可能少地完成它?


更复杂的例子:

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# 中实现,因为将 lambda 分配给 Expression由编译器完成,有点像编译时间 const表达文字。这类似于让编译器理解纯字符串文字 "test"理解模板字符串文字 "test ${a+b} other"并且 C# 编译器尚未处于开发的这个阶段。

所以我的主要问题实际上是:

哪种 CLR 语言支持的语法可以让我方便地构建由其他函数构建的表达式树嵌入部分?

其他可能性是一些库可以帮助我使用某种运行时编译模板以这种方式构建表达式树,但我猜这样我会失去表达式代码的代码完成。


似乎 F# 具有“引用”和“取消引用”(拼接)代码的能力:

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

最佳答案

对于您的两个示例,这实际上可以通过两个表达式访问者来完成(代码已注释):

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.

关于c# - 从 lambda 创建表达式树时如何为 'unquote'?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40810227/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com