美好的一天。表达式树,尤其是与Visitor模式结合使用时,一直是一个令人困惑的话题。因此,有关此主题的信息越丰富,示例越多,那些有兴趣的人将更容易找到对他们而言清晰而有用的内容。
这篇文章照常构建-从概念框架和定义开始,以示例和使用方式结束。目录如下。表达式树的基本知识表达式树的语法表达式类型模式匹配天真的来访者经典来访者嗯,目标不是强加特定的解决方案,也不是说一种解决方案比另一种更好。考虑到您案件中的所有细微差别,我建议自己得出结论。我将以我的榜样发表意见。表达树
基础
首先,您需要处理表达式树。它们表示Expression的类型或其任何继承人(稍后将讨论它们)。在通常情况下,表达式/算法以可执行代码/指令的形式呈现,而用户可能没有太多要做(主要是执行)。 Expression类型允许您将表达式/算法(通常为lambda,但不是必需的)表示为以用户可以访问的树结构组织的数据。以树状方式组织有关算法和类名称的信息的方式为我们提供了“表达式树”。为了清楚起见,我们将分析一个简单的示例。假设我们有lambda(x)=> Console.WriteLine(x + 5)这可以表示为以下树树的根是MethodCall的顶部,方法的参数也是表达式,因此它可以具有任意数量的子代。在我们的例子中,只有一个后代-“ ArithmeticOperation ” 的峰值。它包含有关它是哪种操作的信息,左右操作数也是表达式。这样的顶点将始终具有2个后代。操作数由常量(Constant)和参数(Parameter)表示。这样的表达没有后代。这些是非常简化的示例,但充分体现了本质。表达式树的主要特征是可以对其进行解析,并读取有关算法应执行的所有必要信息。从某些角度来看,这与属性相反。属性是对行为进行声明式描述的一种手段(非常有条件,但最终目标大致相同)。而表达式树正在使用函数/算法来描述数据。例如,它们在实体框架提供程序中使用。该应用程序很明显-解析表达式树,了解在那里应该执行的操作,并根据此描述制作SQL。鲜为人知的例子是用于moking的moq库。表达式树也用于DLR。(动态语言运行时)。编译器开发人员使用它们来确保动态特性和dotnet之间的兼容性,而不是生成MSIL。还值得一提的是,表达式树是不可变的。句法
接下来值得讨论的是语法。有两种主要方法:- 通过Expression类的静态方法创建表达式树
- 使用在Expression中编译的Lambda表达式
表达式类的静态方法
通过Expression类的静态方法创建表达式树的使用较少(尤其是从用户的角度来看)。这很麻烦,但很简单,我们有许多基本的积木可供使用,您可以用它们来构建相当复杂的东西。创建是通过静态方法进行的,因为表达式构造器有一个内部修饰符。但这并不意味着您需要发现反射。例如,我将从上面的示例创建一个表达式:(x)=> Console.WriteLine(x + 5)ParameterExpression parameter = Expression.Parameter(typeof(double));
ConstantExpression constant = Expression.Constant(5d, typeof(double));
BinaryExpression add = Expression.Add(parameter, constant);
MethodInfo writeLine = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(double) });
MethodCallExpression methodCall = Expression.Call(null, writeLine, add);
Expression<Action<double>> expressionlambda = Expression.Lambda<Action<double>>(methodCall, parameter);
Action<double> delegateLambda = expressionlambda.Compile();
delegateLambda(123321);
这可能不是很方便的方法,但是它完全反映了表达式树的内部结构。另外,此方法提供了更多的功能和可在表达式树中使用的功能:从循环,条件,try-catch,goto,赋值,以故障块结尾,调试信息,断点,动态等。Lambda表达式
使用lambda作为表达式是一种更常见的方法。它的工作非常简单-编译阶段的智能编译器查看lambda的用途。并将其编译为委托或表达式。在一个已经掌握的示例中,它看起来如下Expression<Action<double>> write = => Console.WriteLine( + 5);
值得澄清这样的事情-表达式是详尽的描述。这足以获得结果。诸如LambdaExpression或其后代之类的表达式树可以转换为可执行IL。其余类型不能直接转换为可执行代码(但这没有多大意义)。顺便说一句,如果有人批评快速编译表达式,可以看看这个第三方项目。相反,在一般情况下是不正确的。代表不能只是拿起它并以表达方式介绍自己(但这仍然是可能的)。并非所有的lambda都可以转换为表达式树。这些包括:double variable;
dynamic dynamic;
Expression<Action> assignment = () => variable = 5;
Expression<Func<double>> dynamically = () => dynamic;
Expression<Func<Task>> asynchon = async () => await Task.CompletedTask;
Expression<Action> body = () => { };
表达式类型
我建议快速浏览可用的类型,以表示我们拥有的机会。所有这些都位于System.Linq.Expressions命名空间中,我建议您先熟悉一些真正有趣且不寻常的功能。我将在平板电脑中放在一起的简单类型的表达式进行简要说明。动态
使用DynamicExpression,可以在表达式树中使用动态及其所有功能。有一个相当混乱的API,我在本示例中使用的时间比在所有其他示例中使用的时间更长。所有的困惑都是由一堆各种各样的标志提供的。其中一些与您要寻找的类似,但不一定。当使用动态表达式树时,很难说出一个错误。例:var parameter1 = Expression.Parameter(typeof(object), "name1");
var parameter2 = Expression.Parameter(typeof(object), "name2");
var dynamicParam1 = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
var dynamicParam2 = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
CallSiteBinder csb = Microsoft.CSharp.RuntimeBinder.Binder.BinaryOperation(CSharpBinderFlags.None, ExpressionType.Add, typeof(Program), new[] { dynamicParam1, dynamicParam2 });
var dyno = Expression.Dynamic(csb, typeof(object), parameter1, parameter2);
Expression<Func<dynamic, dynamic, dynamic>> expr = Expression.Lambda<Func<dynamic, dynamic, dynamic>>(dyno, new[] { parameter1, parameter2 });
Func<dynamic, dynamic, dynamic> action = expr.Compile();
var res = action("1", "2");
Console.WriteLine(res);
res = action(1, 2);
Console.WriteLine(res);
我明确指出了活页夹的来源,以避免与System.Reflection中的活页夹混淆。有趣的是,我们可以执行ref和out参数,命名参数,一元运算,并且原则上可以通过动态完成所有操作,但这需要一些技巧。异常捕获块
我要注意的第二件事是try / catch / finally / fault功能,或者说,我们可以访问Fault块。它在C#中不可用,但在MSIL中可用,这是一种最终模拟,将在发生任何异常的情况下执行。在下面的示例中,将引发异常,此后将显示“ Hi”,程序将等待输入。只有这样,它才会完全掉落。我不建议您使用这种做法。var throwSmth = Expression.Throw(Expression.Constant(new Exception(), typeof(Exception)));
var log = Expression.Call(null, typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) }), Expression.Constant("Hi", typeof(string)));
var read = Expression.Call(null, typeof(Console).GetMethod(nameof(Console.ReadLine)));
var fault = Expression.TryFault(throwSmth, Expression.Block(new[] { log, read }));
Expression<Action> expr = Expression.Lambda<Action>(fault);
Action compiledExpression = expr.Compile();
compiledExpression();
可用的表达式树类型的简要说明表
- ExpressionVisitor — . .
- DynamicExpressionVisitor — DynamicExpression ( VisitDynamic)
此信息足以开始比较使用表达式树的方法。我决定以找到导数的例子来解析所有这一切。我没有预见所有可能的选择,只有基本的选择。但是,如果由于某种原因有人决定修改和使用它,我将很高兴通过对我的存储库的请求分享改进。模式匹配
因此,任务是对微分进行演算。您可以估算以下内容:对于不同类型的运算-乘法,除法等,有一些规则可以找到导数。根据操作,您必须选择一个特定的公式。在这种平庸的表述中,理想地将任务放在开关/盒上。在最新版本的语言中,我们看到了switch / case 2.0或模式匹配。这里很难讨论一些事情。在集线器上,如此大量的代码看起来很繁琐且阅读效果很差,所以我建议您看一下github。用一个导数的例子,结果像这样例 public class PatterntMatchingDerivative
{
private readonly MethodInfo _pow = typeof(Math).GetMethod(nameof(Math.Pow));
private readonly MethodInfo _log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
private readonly ConstantExpression _zero = Expression.Constant(0d, typeof(double));
private readonly ConstantExpression _one = Expression.Constant(1d, typeof(double));
public Expression<Func<double, double>> ParseDerivative(Expression<Func<double, double>> function)
{
return Expression.Lambda<Func<double, double>>(ParseDerivative(function.Body), function.Parameters);
}
private Expression ParseDerivative(Expression function) => function switch
{
BinaryExpression binaryExpr => function.NodeType switch
{
ExpressionType.Add => Expression.Add(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),
ExpressionType.Subtract => Expression.Subtract(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),
ExpressionType.Multiply => (binaryExpr.Left, binaryExpr.Right) switch
{
(ConstantExpression _, ConstantExpression _) => _zero,
(ConstantExpression constant, ParameterExpression _) => constant,
(ParameterExpression _, ConstantExpression constant) => constant,
_ => Expression.Add(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right)))
},
ExpressionType.Divide => (binaryExpr.Left, binaryExpr.Right) switch
{
(ConstantExpression _, ConstantExpression _) => _zero,
(ConstantExpression constant, ParameterExpression parameter) => Expression.Divide(constant, Expression.Multiply(parameter, parameter)),
(ParameterExpression _, ConstantExpression constant) => Expression.Divide(_one, constant),
_ => Expression.Divide(Expression.Subtract(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right))
},
},
MethodCallExpression methodCall when methodCall.Method == _pow => (methodCall.Arguments[0], methodCall.Arguments[1]) switch
{
(ConstantExpression constant, ParameterExpression _) => Expression.Multiply(methodCall, Expression.Call(null, _log, constant)),
(ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, _pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
(ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(ParseDerivative(expression), methodCall), Expression.Call(null, _log, constant)),
},
_ => function.NodeType switch
{
ExpressionType.Constant => _zero,
ExpressionType.Parameter => _one,
_ => throw new OutOfMemoryException("Bitmap best practice")
}
};
}
看起来有点不寻常,但很有趣。很高兴写这篇文章-所有条件有机地结合在一起。该示例说明了一切,您无法用文字更好地描述它。天真的访客
在这样的任务中,表情树访客立即浮现在脑海,这在粉丝中引起了很大的轰动,并引起了粉丝的恐慌,他们在厨房里讨论敏捷问题。“不要惧怕无知,而要知道虚假的知识。最好什么都不知道,而不是考虑真相,那是不正确的。” 记住托尔斯泰的这个美妙短语,承认无知并争取Google的支持,您可以找到以下指南。我拥有的链接是查询“表达式树访问者”的第一个链接(在1949年西伯利亚之后)。乍一看,这正是我们所需要的。本文的标题符合我们的意愿,示例中的类均以后缀Visitor命名。在回顾了文章并进行了类推之后,通过类比我们的派生示例,我们得到:链接到github。例public class CustomDerivativeExpressionTreeVisitor
{
public Expression<Func<double, double>> Visit(Expression<Func<double, double>> function)
{
return Expression.Lambda<Func<double, double>>(Visitor.CreateFromExpression(function.Body).Visit(), function.Parameters);
}
}
public abstract class Visitor
{
protected static readonly MethodInfo Pow = typeof(Math).GetMethod(nameof(Math.Pow));
protected static readonly MethodInfo Log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
protected readonly ConstantExpression Zero = Expression.Constant(0d, typeof(double));
protected readonly ConstantExpression One = Expression.Constant(1d, typeof(double));
public abstract Expression Visit();
public static Visitor CreateFromExpression(Expression node)
=> node switch
{
BinaryExpression be => new BinaryVisitor(be),
MethodCallExpression mce when mce.Method == Pow => new PowMethodCallVisitor(mce),
_ => new SimpleVisitor(node),
};
}
public class BinaryVisitor : Visitor
{
private readonly BinaryExpression _node;
public BinaryVisitor(BinaryExpression node)
{
_node = node;
}
public override Expression Visit()
=> _node.NodeType switch
{
ExpressionType.Add => Expression.Add(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),
ExpressionType.Subtract => Expression.Subtract(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),
ExpressionType.Multiply => (binaryExpr.Left, binaryExpr.Right) switch
{
(ConstantExpression _, ConstantExpression _) => _zero,
(ConstantExpression constant, ParameterExpression _) => constant,
(ParameterExpression _, ConstantExpression constant) => constant,
_ => Expression.Add(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right)))
},
ExpressionType.Divide => (binaryExpr.Left, binaryExpr.Right) switch
{
(ConstantExpression _, ConstantExpression _) => _zero,
(ConstantExpression constant, ParameterExpression parameter) => Expression.Divide(constant, Expression.Multiply(parameter, parameter)),
(ParameterExpression _, ConstantExpression constant) => Expression.Divide(_one, constant),
_ => Expression.Divide(Expression.Subtract(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right))
},
};
}
public class PowMethodCallVisitor : Visitor
{
private readonly MethodCallExpression _node;
public PowMethodCallVisitor(MethodCallExpression node)
{
_node = node;
}
public override Expression Visit()
=> (_node.Arguments[0], _node.Arguments[1]) switch
{
(ConstantExpression constant, ParameterExpression _) => Expression.Multiply(_node, Expression.Call(null, Log, constant)),
(ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, Pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
(ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(CreateFromExpression(expression).Visit(), _node), Expression.Call(null, Log, constant)),
};
}
public class SimpleVisitor : Visitor
{
private readonly Expression _node;
public SimpleVisitor(Expression node)
{
_node = node;
}
public override Expression Visit()
=> _node.NodeType switch
{
ExpressionType.Constant => Zero,
ExpressionType.Parameter => One,
};
}
实际上-我们将开关案例分散到不同的类别。他们不少,魔法没有出现。所有相同的情况,更多行。承诺的双重派遣在哪里?经典访客和双重调度
在这里,有必要介绍Visitor模板本身,它也是Visitor,这是Expression tree visitor的基础。让我们仅以表达式树为例进行分析。第二,假设我们设计了表达式树。我们希望使用户能够遍历表达式树,并根据节点的类型(表达式类型)执行某些操作。第一种选择是什么也不做。也就是说,强迫用户使用开关/外壳。这不是一个坏选择。但是这里有一个细微差别:我们散布负责特定类型的逻辑。简而言之,多态性和虚拟挑战(也称为后期绑定)),可以将类型定义移至运行时,并从我们的代码中删除这些检查。对于我们而言,具有创建所需类型的实例的逻辑就足够了,然后一切将由运行时为我们完成。第二种选择。显而易见的解决方案是将逻辑引入虚拟方法。通过覆盖每个后继对象中的虚拟方法,我们可以忽略开关/大小写。多态调用的机制将为我们决定。方法表将在此处工作,方法将通过其中的偏移量进行调用。但这是整篇文章的主题,所以我们不要着迷。虚拟方法似乎可以解决我们的问题。但是不幸的是,他们创造了另一个。对于我们的任务,我们可以添加GetDeriviative()方法。但是现在表达式类本身看起来很奇怪。我们可以在所有场合添加此类方法,但它们不适合该类的一般逻辑。而且,我们仍然没有提供进行类似于用户的操作的机会(当然是通过适当的方式)。我们需要让用户为每种特定类型定义逻辑,但保留多态性(我们可以使用)。仅用户为此付出的努力不会成功。这才是真正的访客所在。在层次结构的基本类型(本例中为Expression)中,我们定义了以下形式的方法virtual Expression Accept(ExpressionVisitor visitor);
在继承人中,此方法将被覆盖。ExpressionVisitor本身是一个基类,其中包含对每种层次结构类型都具有相同签名的虚拟方法。以ExpressionVisitor类为例,VisitBinary(...),VisitMethodCall(...),VisitConstant(...),VisitParameter(...)。这些方法在层次结构的相应类中调用。那些。BinaryExpression类中的Accept方法将如下所示:
protected internal override Expression Accept(ExpressionVisitor visitor)
{
return visitor.VisitBinary(this);
}
结果,为了定义新的行为,用户只需要创建ExpressionVisitor类的继承人,即可在其中重新定义用于解决一个问题的相应方法。在我们的例子中,创建了DerivativeExpressionVisitor。此外,我们有一些Expression的后继对象,但哪些是未知的,但不是必需的。我们使用所需的ExpressionVisitor实现调用虚拟的Accept方法,即与DerivativeExpressionVisitor一起使用。由于采用了动态调度,因此调用了Accept的重写实现,例如运行时,例如BinaryExpression。在此方法的主体中,我们完全理解我们处于BinaryExpression中,但是我们不知道遇到了哪个ExpressionVisitor继承者。但是由于VisitBinary也是虚拟的,我们不需要知道。同样,我们只是通过引用基类来调用,该调用是动态(在运行时)调度的,并且调用了运行时类型的重写VisitBinary实现。对于双重派遣而言,如此之多-乒乓球的风格就是“你做”-“不,你”。它给我们带来了什么。实际上,这使得可以从外部“添加”虚拟方法,而不是换课。听起来不错,但也有缺点:- 以“接受”方法的形式出现的一些左派分子,它同时负责所有事务
- 一个好的哈希函数的连锁反应是,当您仅向层次结构中添加一个继承人时,在最坏的情况下,每个人都必须完成访问者的操作
但是由于使用表达式的特殊性,表达式树的性质允许这些成本,因为这种解决方法是其主要功能之一。在这里,您可以看到所有可用于重载的方法。因此,让我们看看它的外观。链接到github。例public class BuildinExpressionTreeVisitor : ExpressionVisitor
{
private readonly MethodInfo _pow = typeof(Math).GetMethod(nameof(Math.Pow));
private readonly MethodInfo _log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
private readonly ConstantExpression _zero = Expression.Constant(0d, typeof(double));
private readonly ConstantExpression _one = Expression.Constant(1d, typeof(double));
public Expression<Func<double, double>> GetDerivative(Expression<Func<double, double>> function)
{
return Expression.Lambda<Func<double, double>>(Visit(function.Body), function.Parameters);
}
protected override Expression VisitBinary(BinaryExpression binaryExpr)
=> binaryExpr.NodeType switch
{
ExpressionType.Add => Expression.Add(Visit(binaryExpr.Left), Visit(binaryExpr.Right)),
ExpressionType.Subtract => Expression.Subtract(Visit(binaryExpr.Left), Visit(binaryExpr.Right)),
ExpressionType.Multiply when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ConstantExpression => _zero,
ExpressionType.Multiply when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ParameterExpression => binaryExpr.Left,
ExpressionType.Multiply when binaryExpr.Left is ParameterExpression && binaryExpr.Right is ConstantExpression => binaryExpr.Right,
ExpressionType.Multiply => Expression.Add(Expression.Multiply(Visit(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, Visit(binaryExpr.Right))),
ExpressionType.Divide when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ConstantExpression => _zero,
ExpressionType.Divide when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ParameterExpression => Expression.Divide(binaryExpr.Left, Expression.Multiply(binaryExpr.Right, binaryExpr.Right)),
ExpressionType.Divide when binaryExpr.Left is ParameterExpression && binaryExpr.Right is ConstantExpression => Expression.Divide(_one, binaryExpr.Right),
ExpressionType.Divide => Expression.Divide(Expression.Subtract(Expression.Multiply(Visit(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, Visit(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right)),
};
protected override Expression VisitMethodCall(MethodCallExpression methodCall)
=> (methodCall.Arguments[0], methodCall.Arguments[1]) switch
{
(ConstantExpression constant, ParameterExpression _) => Expression.Multiply(methodCall, Expression.Call(null, _log, constant)),
(ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, _pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
(ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(Visit(expression), methodCall), Expression.Call(null, _log, constant)),
};
protected override Expression VisitConstant(ConstantExpression _) => _zero;
protected override Expression VisitParameter(ParameterExpression b) => _one;
}
发现
也许像大多数编程任务一样,不能给出明确的答案。一如既往,一切都取决于具体情况。我喜欢本例中的常规模式匹配,因为 我没有将其开发到工业发展的规模。如果此表达式将不受控制地增加,则值得考虑访客。甚至是天真的访问者都享有生命权-毕竟,如果层次结构本身没有提供支持,这也是将大量代码分散到类中的一种好方法。即使在这里也有例外。同样,访问者对层次结构的支持也引起了很大争议。但是我希望这里提供的信息足以做出正确的选择。