【问题标题】:Why would I want to use an ExpressionVisitor?为什么要使用 ExpressionVisitor?
【发布时间】:2017-05-16 21:59:13
【问题描述】:

我从 MSDN 关于How to: Modify Expression Trees 的文章中知道ExpressionVisitor 应该做什么。它应该修改表达式。

然而,他们的例子非常不切实际,所以我想知道为什么我需要它?您能否列举一些修改表达式树有意义的真实案例?或者,为什么必须对其进行修改?从什么到什么?

它也有许多访问各种表达式的重载。我怎么知道什么时候应该使用它们以及它们应该返回什么?我看到人们使用VisitParameter 并返回base.VisitParameter(node),而另一方面又返回Expression.Parameter(..)

【问题讨论】:

  • 如果你想修改一个树结构,访问者是一种非常标准的技术来实现这一点,尤其是对于不可变的树。这为此类访问者提供了一个基类。您只需覆盖需要更改的节点类型的方法并返回新节点。访问者作为一个整体逐步构建一棵新树。
  • @LucasTrzesniewski 这是我已经发现的(好吧,没有节点类型,这是我不知道的)我对我以前可以拥有什么以及会出现什么很感兴趣。什么样的表达式树需要从什么修改到什么?这是缺少的链接。
  • 嘿,答案是无论你需要做什么。表达式树不需要 修改, 可能出于某种原因需要修改它们。一个真实的例子见LINQKit(AsExpandable 重写表达式)。
  • 真正的那个是internal,当时我记得复制/粘贴了一个反编译的版本,所以我可以使用它。 MS最终决定将其公开。出于同样的原因,我想 LINQKit 有一个自定义版本,这让它们可以支持旧的框架版本。
  • @t3chb0t MSDN 文档?除此之外,还有信息量很大的codeplex.com/Download?ProjectName=dlr&DownloadId=246540,它既有一些高级细节,也有一些低级细节,虽然它描述的一些事情从未实现过,但它提供了不同的视角。

标签: c# expression-trees expressionvisitor


【解决方案1】:

存在一个问题,即数据库中的字段包含 0 或 1(数字),我们想在应用程序上使用布尔值。

解决方案是创建一个“标志”对象,其中包含 0 或 1 并转换为布尔值。我们在所有应用程序中都像 bool 一样使用它,但是当我们在 .Where() 子句中使用它时,EntityFramework 抱怨它无法调用转换方法。

因此,在将树发送到 EF 之前,我们使用表达式访问者将所有属性访问,如 .Where(x => x.Property) 更改为 .Where(x => x.Property.Value == 1)。

【讨论】:

  • 这真是一个很好的现实例子。
【解决方案2】:

您能否列举一些修改表达式树有意义的真实案例?

严格来说,我们从不修改表达式树,因为它们是不可变的(至少从外部看,它不会在内部记忆值或具有可变的私有状态)。正是因为它们是不可变的,因此我们不能仅仅改变一个节点,如果我们想创建一个基于我们拥有但在某些特定方式上有所不同的新表达式树(最接近我们必须修改不可变对象的事情)。

我们可以在 Linq 本身中找到一些。

在许多方面,最简单的 Linq 提供程序是 linq-to-objects 提供程序,它适用于内存中的可枚举对象。

当它以IEnumerable<T> 对象的形式直接接收可枚举值时,这非常简单,因为大多数程序员可以很快编写出大多数方法的未优化版本。例如。 Where 只是:

foreach (T item in source)
  if (pred(item))
    yield return item;

等等。但是EnumerableQueryable 实现IQueryable<T> 版本呢?由于EnumerableQueryable 包装了IEnumerable<T>,我们可以对所涉及的一个或多个可枚举对象执行所需的操作,但我们有一个表达式以IQueryable<T> 和其他选择器、谓词等表达式来描述该操作,其中我们需要的是用IEnumerable<T> 和选择器、谓词等的委托来描述该操作。

System.Linq.EnumerableRewriterExpressionVisitor 的一个实现,正是这样重写,然后可以简单地编译和执行结果。

System.Linq.Expressions 内部,有一些ExpressionVisitor 的实现用于不同的目的。一个例子是编译的解释器形式不能直接处理引用表达式中的提升变量,因此它使用访问者将其重写为处理字典中的索引。

除了产生另一个表达式之外,ExpressionVisitor 还可以产生另一个结果。同样System.Linq.Expressions 本身有内部示例,带有调试字符串,ToString() 用于许多表达式类型,通过访问相关表达式来工作。

这可以(尽管不是必须)是数据库查询 linq 提供程序用来将表达式转换为 SQL 查询的方法。

我怎么知道什么时候应该使用它们以及它们应该返回什么?

这些方法的默认实现将:

  1. 如果表达式不能有子表达式(例如 Expression.Constant() 的结果),那么它将再次返回节点。
  2. 否则访问所有子表达式,然后在有问题的表达式上调用Update,将结果传回。 Update 反过来将返回与新子节点相同类型的新节点,或者如果子节点未更改,则再次返回相同的节点。

因此,如果您不知道出于任何目的需要显式操作节点,那么您可能不需要更改它。这也意味着Update 是获取新版本节点以进行部分更改的便捷方式。但是,“无论你的目的是什么”意味着什么当然取决于用例。最常见的情况可能会走到一个极端,要么只有一种或两种表达式类型需要覆盖,要么全部或几乎全部都需要覆盖。

(一个警告是,如果您正在检查那些在 ReadOnlyCollection 中具有子节点的节点的子节点,例如 BlockExpression 的步骤和变量或 TryExpression 的捕获块,您只会有时更改这些孩子,那么如果您没有更改,最好自己检查是否存在缺陷 [最近已修复,但尚未在任何发布的版本中] 意味着如果您将相同的孩子传递给不同集合中的 Update原来的ReadOnlyCollection 然后不必要地创建了一个新的表达式,这会影响树的更远位置。这通常是无害的,但会浪费时间和内存)。

【讨论】:

    【解决方案3】:

    ExpressionVisitorExpression 启用visitor pattern

    从概念上讲,问题是当您浏览Expression 树时,您只知道任何给定节点都是Expression,但您不知道具体是哪种Expression。此模式可让您了解您正在使用哪种类型的 Expression,并为不同类型指定特定于类型的处理。

    当您有Expression 时,您可以拨打.ModifyExpression 知道自己的类型,因此它会回调适当的 override

    看着MSDN example you linked

    public class AndAlsoModifier : ExpressionVisitor  
    {  
        public Expression Modify(Expression expression)  
        {  
            return Visit(expression);  
        }  
    
        protected override Expression VisitBinary(BinaryExpression b)  
        {  
            if (b.NodeType == ExpressionType.AndAlso)  
            {  
                Expression left = this.Visit(b.Left);  
                Expression right = this.Visit(b.Right);  
    
                // Make this binary expression an OrElse operation instead of an AndAlso operation.  
                return Expression.MakeBinary(ExpressionType.OrElse, left, right, b.IsLiftedToNull, b.Method);  
            }  
    
            return base.VisitBinary(b);  
        }  
    }
    

    在此示例中,如果 Expression 恰好是 BinaryExpression,它将回调示例中给出的 VisitBinary(BinaryExpression b)。现在,您可以处理BinaryExpression,知道它是BinaryExpression。您还可以指定处理其他类型的Expression 的其他override 方法。

    值得注意的是,由于这是一个重载的解析技巧,访问Expression's 将回调最佳拟合方法。所以,如果有不同种类的BinaryExpression,那么你可以为一个特定的子类型写一个override;如果另一个子类型回调,它将只使用默认的BinaryExpression 处理。

    简而言之,此模式允许您导航 Expression 树,了解您正在使用哪种类型的 Expression

    【讨论】:

    • 哦,我根本没有将它与访问者模式相关联。我喜欢您的解释,希望通过另一个示例,我开始了解它。我实际上需要知道我想要操作的表达式树,以便我知道我应该在哪个节点停止,查看并在必要时修改我需要它的工作方式/行为。
    • @t3chb0t 听起来您找到了正确的工具,那么!您可以通过其他方式获得此好处(通常被描述为double dispatch),但访问者模式是一种流行的方式。
    【解决方案4】:

    在转移到 EF Core 并从 Sql Server(MS 特定)迁移到 SqlLite(独立于平台)时,我刚刚遇到了具体的现实世界示例。

    现有的业务逻辑围绕中间层/服务层接口展开,假设全文搜索 (FTS) 在后台自动发生,与 SQL Server 一样。搜索相关查询通过表达式和 FTS 传递到此层,针对 Sql Server 存储不需要额外的 FTS 特定实体。

    我不想更改任何内容,但是使用 SqlLite,您必须针对全文搜索的特定虚拟表,这反过来意味着更改所有中间层调用以重新定位 FTS 表/实体然后将它们加入业务实体表以获得类似的结果集。

    但是通过对 ExpressionVisitor 进行子类化,我能够拦截 DAL 层中的调用并简单地重写传入的表达式(或更准确地说是整个搜索表达式中的一些 BinaryExpressions)来专门处理 SqlLites FTS 要求。

    这意味着数据层对数据存储的专门化发生在从存储库基类中的单个位置调用的单个类中。无需更改应用程序的其他方面即可通过 EFCore 支持 FTS,并且任何 SqlLite FTS 相关实体都可以包含在单个可插入程序集中。

    所以 ExpressionVisitor 真的非常有用,尤其是结合能够通过各种形式的 IPC 将表达式树作为数据传递的整个概念时。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-08-22
      • 2016-09-12
      • 1970-01-01
      • 2020-11-07
      • 2011-07-27
      • 2020-05-05
      相关资源
      最近更新 更多