解释表达式

下面的代码示例演示如何将表示 lambda 表达式 num => num < 5 的表达式树分解为其部分。

// Add the following using directive to your code file:
// using System.Linq.Expressions;

// Create an expression tree.
Expression<Func<int, bool>> exprTree = num => num < 5;

// Decompose the expression tree.
ParameterExpression param = (ParameterExpression)exprTree.Parameters[0];
BinaryExpression operation = (BinaryExpression)exprTree.Body;
ParameterExpression left = (ParameterExpression)operation.Left;
ConstantExpression right = (ConstantExpression)operation.Right;

Console.WriteLine($"Decomposed expression: {param.Name} => {left.Name} {operation.NodeType} {right.Value}");

// This code produces the following output:

// Decomposed expression: num => num LessThan 5

现在,让我们编写一些代码来检查 表达式树的结构。 表达式树中的每个节点都是派生自 Expression的类的对象。

该设计使访问表达式树中的所有节点是相对简单的递归作。 一般策略是从根节点开始,并确定它的节点类型。

如果节点类型具有子级,则以递归方式访问该子级。 在每个子节点中,重复在根节点处使用的步骤:确定类型,且如果该类型具有子级,则访问每个子级。

检查不具有子级的表达式

首先,访问简单表达式树中的每个节点。 下面是创建常量表达式并检查其属性的代码:

var constant = Expression.Constant(24, typeof(int));

Console.WriteLine($"This is a/an {constant.NodeType} expression type");
Console.WriteLine($"The type of the constant value is {constant.Type}");
Console.WriteLine($"The value of the constant value is {constant.Value}");

前面的代码输出以下输出:

This is a/an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 24

现在,让我们编写代码来检查此表达式并写出一些有关它的重要属性。

加法表达式

让我们从本部分简介中的新增示例开始。

Expression<Func<int>> sum = () => 1 + 2;

注释

请勿使用var来声明此表达式树,因为委托的自然类型是Func<int>,不是Expression<Func<int>>

根节点是一个 LambdaExpression。 为了获得 => 运算符右侧的有用代码,需要找到 LambdaExpression 的子级之一。 你需要用本节中的所有表达式来完成这一操作。 父节点确实帮助我们确定LambdaExpression的返回类型。

若要检查此表达式中的每个节点,需要以递归方式访问多个节点。 下面是一个简单的第一个实现:

Expression<Func<int, int, int>> addition = (a, b) => a + b;

Console.WriteLine($"This expression is a {addition.NodeType} expression type");
Console.WriteLine($"The name of the lambda is {((addition.Name == null) ? "<null>" : addition.Name)}");
Console.WriteLine($"The return type is {addition.ReturnType.ToString()}");
Console.WriteLine($"The expression has {addition.Parameters.Count} arguments. They are:");
foreach (var argumentExpression in addition.Parameters)
{
    Console.WriteLine($"\tParameter Type: {argumentExpression.Type.ToString()}, Name: {argumentExpression.Name}");
}

var additionBody = (BinaryExpression)addition.Body;
Console.WriteLine($"The body is a {additionBody.NodeType} expression");
Console.WriteLine($"The left side is a {additionBody.Left.NodeType} expression");
var left = (ParameterExpression)additionBody.Left;
Console.WriteLine($"\tParameter Type: {left.Type.ToString()}, Name: {left.Name}");
Console.WriteLine($"The right side is a {additionBody.Right.NodeType} expression");
var right = (ParameterExpression)additionBody.Right;
Console.WriteLine($"\tParameter Type: {right.Type.ToString()}, Name: {right.Name}");

此示例输出以下输出:

This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 arguments. They are:
        Parameter Type: System.Int32, Name: a
        Parameter Type: System.Int32, Name: b
The body is a/an Add expression
The left side is a Parameter expression
        Parameter Type: System.Int32, Name: a
The right side is a Parameter expression
        Parameter Type: System.Int32, Name: b

在前面的代码示例中,你会注意到很多重复。 让我们将其清理干净,并生成一个更加通用的表达式节点访问者。 这需要我们编写递归算法。 任何节点都可能是具有子级的类型。 具有子级的任何节点都要求访问这些子级并确定该节点是什么。 下面是利用递归访问加法运算的已清理的版本:

using System.Linq.Expressions;

namespace Visitors;
// Base Visitor class:
public abstract class Visitor
{
    private readonly Expression node;

    protected Visitor(Expression node) => this.node = node;

    public abstract void Visit(string prefix);

    public ExpressionType NodeType => node.NodeType;
    public static Visitor CreateFromExpression(Expression node) =>
        node.NodeType switch
        {
            ExpressionType.Constant => new ConstantVisitor((ConstantExpression)node),
            ExpressionType.Lambda => new LambdaVisitor((LambdaExpression)node),
            ExpressionType.Parameter => new ParameterVisitor((ParameterExpression)node),
            ExpressionType.Add => new BinaryVisitor((BinaryExpression)node),
            _ => throw new NotImplementedException($"Node not processed yet: {node.NodeType}"),
        };
}

// Lambda Visitor
public class LambdaVisitor : Visitor
{
    private readonly LambdaExpression node;
    public LambdaVisitor(LambdaExpression node) : base(node) => this.node = node;

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This expression is a {NodeType} expression type");
        Console.WriteLine($"{prefix}The name of the lambda is {((node.Name == null) ? "<null>" : node.Name)}");
        Console.WriteLine($"{prefix}The return type is {node.ReturnType}");
        Console.WriteLine($"{prefix}The expression has {node.Parameters.Count} argument(s). They are:");
        // Visit each parameter:
        foreach (var argumentExpression in node.Parameters)
        {
            var argumentVisitor = CreateFromExpression(argumentExpression);
            argumentVisitor.Visit(prefix + "\t");
        }
        Console.WriteLine($"{prefix}The expression body is:");
        // Visit the body:
        var bodyVisitor = CreateFromExpression(node.Body);
        bodyVisitor.Visit(prefix + "\t");
    }
}

// Binary Expression Visitor:
public class BinaryVisitor : Visitor
{
    private readonly BinaryExpression node;
    public BinaryVisitor(BinaryExpression node) : base(node) => this.node = node;

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This binary expression is a {NodeType} expression");
        var left = CreateFromExpression(node.Left);
        Console.WriteLine($"{prefix}The Left argument is:");
        left.Visit(prefix + "\t");
        var right = CreateFromExpression(node.Right);
        Console.WriteLine($"{prefix}The Right argument is:");
        right.Visit(prefix + "\t");
    }
}

// Parameter visitor:
public class ParameterVisitor : Visitor
{
    private readonly ParameterExpression node;
    public ParameterVisitor(ParameterExpression node) : base(node)
    {
        this.node = node;
    }

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This is an {NodeType} expression type");
        Console.WriteLine($"{prefix}Type: {node.Type}, Name: {node.Name}, ByRef: {node.IsByRef}");
    }
}

// Constant visitor:
public class ConstantVisitor : Visitor
{
    private readonly ConstantExpression node;
    public ConstantVisitor(ConstantExpression node) : base(node) => this.node = node;

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This is an {NodeType} expression type");
        Console.WriteLine($"{prefix}The type of the constant value is {node.Type}");
        Console.WriteLine($"{prefix}The value of the constant value is {node.Value}");
    }
}

此算法是用于访问任意LambdaExpression的一个算法的基础。 你创建的代码只查找可能遇到的表达式树节点集的小示例。 但是,你仍然可以从它产生的内容中学到相当多的一些知识。 (当遇到新节点类型时,该方法中的 Visitor.CreateFromExpression 默认情况会将消息输出到错误控制台。这样,你知道要添加新的表达式类型。

在前面的加法表达式上运行此访问者时,将得到以下输出:

This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 argument(s). They are:
        This is an Parameter expression type
        Type: System.Int32, Name: a, ByRef: False
        This is an Parameter expression type
        Type: System.Int32, Name: b, ByRef: False
The expression body is:
        This binary expression is a Add expression
        The Left argument is:
                This is an Parameter expression type
                Type: System.Int32, Name: a, ByRef: False
        The Right argument is:
                This is an Parameter expression type
                Type: System.Int32, Name: b, ByRef: False

现在,你已构建了更常规的访问者实现,接下来可以访问和处理更多不同类型的表达式。

具有更多操作数的加法表达式

让我们尝试一个更复杂的示例,但仍将节点类型限制为仅添加:

Expression<Func<int>> sum = () => 1 + 2 + 3 + 4;

在访问者算法上运行这些示例之前,请尝试一个思考练习来找出输出可能是什么。 请记住,+ 该运算符是二元运算符:它必须有两个子节点,分别表示左操作数和右操作数。 有几种可行的方法来构造可能正确的树:

Expression<Func<int>> sum1 = () => 1 + (2 + (3 + 4));
Expression<Func<int>> sum2 = () => ((1 + 2) + 3) + 4;

Expression<Func<int>> sum3 = () => (1 + 2) + (3 + 4);
Expression<Func<int>> sum4 = () => 1 + ((2 + 3) + 4);
Expression<Func<int>> sum5 = () => (1 + (2 + 3)) + 4;

可以看到可能的答案分为两种,以便着重于最有可能正确的答案。 第一个表示 右关联 表达式。 第二个表示 左关联 表达式。 这两种格式的优点是能适应任意数量的加法表达式。

如果确实通过该访问者运行此表达式,则会看到此输出,它验证简单的加法表达式是否为左结合

若要运行此示例并查看完整的表达式树,请对源表达式树进行更改。 当表达式树包含所有常量时,生成的树只包含常量值 10。 编译器执行所有添加作,并将表达式减少为最简单的形式。 只需在表达式中添加一个变量即可查看原始树:

Expression<Func<int, int>> sum = (a) => 1 + a + 3 + 4;

创建可得出此总和的访问者并运行该访问者,会看到以下输出:

This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 1 argument(s). They are:
        This is an Parameter expression type
        Type: System.Int32, Name: a, ByRef: False
The expression body is:
        This binary expression is a Add expression
        The Left argument is:
                This binary expression is a Add expression
                The Left argument is:
                        This binary expression is a Add expression
                        The Left argument is:
                                This is an Constant expression type
                                The type of the constant value is System.Int32
                                The value of the constant value is 1
                        The Right argument is:
                                This is an Parameter expression type
                                Type: System.Int32, Name: a, ByRef: False
                The Right argument is:
                        This is an Constant expression type
                        The type of the constant value is System.Int32
                        The value of the constant value is 3
        The Right argument is:
                This is an Constant expression type
                The type of the constant value is System.Int32
                The value of the constant value is 4

可以通过访问者代码运行任何其他示例,并查看它所表示的树。 下面是上述 sum3 表达式的示例(还有一个用于防止编译器计算常量的其他参数):

Expression<Func<int, int, int>> sum3 = (a, b) => (1 + a) + (3 + b);

下面是访问者的输出:

This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 argument(s). They are:
        This is an Parameter expression type
        Type: System.Int32, Name: a, ByRef: False
        This is an Parameter expression type
        Type: System.Int32, Name: b, ByRef: False
The expression body is:
        This binary expression is a Add expression
        The Left argument is:
                This binary expression is a Add expression
                The Left argument is:
                        This is an Constant expression type
                        The type of the constant value is System.Int32
                        The value of the constant value is 1
                The Right argument is:
                        This is an Parameter expression type
                        Type: System.Int32, Name: a, ByRef: False
        The Right argument is:
                This binary expression is a Add expression
                The Left argument is:
                        This is an Constant expression type
                        The type of the constant value is System.Int32
                        The value of the constant value is 3
                The Right argument is:
                        This is an Parameter expression type
                        Type: System.Int32, Name: b, ByRef: False

请注意,括号不是输出的一部分。 表达式树中没有表示输入表达式中的括号的节点。 表达式树的结构包含传达优先级所需的所有信息。

扩展此示例

该示例仅处理最基本的表达式树。 在本部分中看到的代码仅处理常量整数和二进制 + 运算符。 作为最后一个示例,让我们更新访问者以处理更加复杂的表达式。 让我们将它用于以下阶乘表达式:

Expression<Func<int, int>> factorial = (n) =>
    n == 0 ?
    1 :
    Enumerable.Range(1, n).Aggregate((product, factor) => product * factor);

此代码表示数学 因子 函数的一个可能实现。 编写此代码的方式强调了通过将 Lambda 表达式分配到表达式来生成表达式树的两个限制。 首先,不允许使用带有语句的 lambda 表达式。 这意味着不能使用循环、块、if/else 语句和其他 C# 中常见的控制结构。 你只能使用表达式。 其次,不能以递归方式调用同一表达式。 如果该表达式已是一个委托,则可以通过递归方式进行调用,但不能在其表达式树的形式中调用它。 在 生成表达式树的部分中,你将学习克服这些限制的技术。

在此表达式中,你将遇到所有这些类型的节点:

  1. 相等(二进制表达式)
  2. 乘法(二进制表达式)
  3. 条件表达式(? :
  4. 方法调用表达式(调用 Range()Aggregate()

修改访问者算法的一种方法是继续执行该算法,并在每次到达 default 子句时写入节点类型。 经过几次迭代,可以看到每个潜在节点。 那么,你已经拥有了一切所需。 结果如下所示:

public static Visitor CreateFromExpression(Expression node) =>
    node.NodeType switch
    {
        ExpressionType.Constant    => new ConstantVisitor((ConstantExpression)node),
        ExpressionType.Lambda      => new LambdaVisitor((LambdaExpression)node),
        ExpressionType.Parameter   => new ParameterVisitor((ParameterExpression)node),
        ExpressionType.Add         => new BinaryVisitor((BinaryExpression)node),
        ExpressionType.Equal       => new BinaryVisitor((BinaryExpression)node),
        ExpressionType.Multiply    => new BinaryVisitor((BinaryExpression) node),
        ExpressionType.Conditional => new ConditionalVisitor((ConditionalExpression) node),
        ExpressionType.Call        => new MethodCallVisitor((MethodCallExpression) node),
        _ => throw new NotImplementedException($"Node not processed yet: {node.NodeType}"),
    };

ConditionalVisitorMethodCallVisitor处理这两个节点。

public class ConditionalVisitor : Visitor
{
    private readonly ConditionalExpression node;
    public ConditionalVisitor(ConditionalExpression node) : base(node)
    {
        this.node = node;
    }

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This expression is a {NodeType} expression");
        var testVisitor = Visitor.CreateFromExpression(node.Test);
        Console.WriteLine($"{prefix}The Test for this expression is:");
        testVisitor.Visit(prefix + "\t");
        var trueVisitor = Visitor.CreateFromExpression(node.IfTrue);
        Console.WriteLine($"{prefix}The True clause for this expression is:");
        trueVisitor.Visit(prefix + "\t");
        var falseVisitor = Visitor.CreateFromExpression(node.IfFalse);
        Console.WriteLine($"{prefix}The False clause for this expression is:");
        falseVisitor.Visit(prefix + "\t");
    }
}

public class MethodCallVisitor : Visitor
{
    private readonly MethodCallExpression node;
    public MethodCallVisitor(MethodCallExpression node) : base(node)
    {
        this.node = node;
    }

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This expression is a {NodeType} expression");
        if (node.Object == null)
            Console.WriteLine($"{prefix}This is a static method call");
        else
        {
            Console.WriteLine($"{prefix}The receiver (this) is:");
            var receiverVisitor = Visitor.CreateFromExpression(node.Object);
            receiverVisitor.Visit(prefix + "\t");
        }

        var methodInfo = node.Method;
        Console.WriteLine($"{prefix}The method name is {methodInfo.DeclaringType}.{methodInfo.Name}");
        // There is more here, like generic arguments, and so on.
        Console.WriteLine($"{prefix}The Arguments are:");
        foreach (var arg in node.Arguments)
        {
            var argVisitor = Visitor.CreateFromExpression(arg);
            argVisitor.Visit(prefix + "\t");
        }
    }
}

表达式树的输出将为:

This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 1 argument(s). They are:
        This is an Parameter expression type
        Type: System.Int32, Name: n, ByRef: False
The expression body is:
        This expression is a Conditional expression
        The Test for this expression is:
                This binary expression is a Equal expression
                The Left argument is:
                        This is an Parameter expression type
                        Type: System.Int32, Name: n, ByRef: False
                The Right argument is:
                        This is an Constant expression type
                        The type of the constant value is System.Int32
                        The value of the constant value is 0
        The True clause for this expression is:
                This is an Constant expression type
                The type of the constant value is System.Int32
                The value of the constant value is 1
        The False clause for this expression is:
                This expression is a Call expression
                This is a static method call
                The method name is System.Linq.Enumerable.Aggregate
                The Arguments are:
                        This expression is a Call expression
                        This is a static method call
                        The method name is System.Linq.Enumerable.Range
                        The Arguments are:
                                This is an Constant expression type
                                The type of the constant value is System.Int32
                                The value of the constant value is 1
                                This is an Parameter expression type
                                Type: System.Int32, Name: n, ByRef: False
                        This expression is a Lambda expression type
                        The name of the lambda is <null>
                        The return type is System.Int32
                        The expression has 2 arguments. They are:
                                This is an Parameter expression type
                                Type: System.Int32, Name: product, ByRef: False
                                This is an Parameter expression type
                                Type: System.Int32, Name: factor, ByRef: False
                        The expression body is:
                                This binary expression is a Multiply expression
                                The Left argument is:
                                        This is an Parameter expression type
                                        Type: System.Int32, Name: product, ByRef: False
                                The Right argument is:
                                        This is an Parameter expression type
                                        Type: System.Int32, Name: factor, ByRef: False

扩充示例库

本节中的示例显示了访问和检查表达式树中的节点的核心技术。 它简化了你将遇到的节点类型,以专注于在表达式树中访问和访问节点的核心任务。

首先,访问者仅处理整数常量。 常量值可以是任何其他数值类型,C# 语言支持在这些类型之间进行转换和提升。 此代码的更可靠版本将反映所有这些功能。

即使是最后一个示例也能识别可能的节点类型的子集。 你仍然可以为它馈送许多导致它失败的表达式。 在 .NET Standard 中,名称为 ExpressionVisitor 的完整实现可以处理所有可能的节点类型。

最后,本文中使用的库是为演示和学习而构建的。 它没有经过优化。 它使结构清晰,并突出显示用于访问节点并分析其内容的技术。

即使存在这些限制,你也应该能够很好地编写读取和理解表达式树的算法。