我为什么要使用ExpressionVisitor?

时间:2017-01-02 20:21:52

标签: c# expression-trees expressionvisitor

我从MSDN的文章中了解How to: Modify Expression Trees <?php namespace App\Http\Controllers; use App\CMS\Page; use Illuminate\Http\Request; use App\Http\Requests; class CoursePageController extends Controller { /** * Display the specified resource. * @param $course_slug * @param $slug * @return \Illuminate\Http\Response * @internal param int $id */ public function show($course_slug, $slug) { $page = $this->getPage($course_slug, $slug); return view($page->layout->templateFullPath()) ->withPage($page); } /** * Gets page by slug. * @param $course_slug * @param $slug * @return mixed */ protected function getPage($course_slug, $slug) { $page = Page::with('layout', 'contentRegions')->whereSlug($slug)->first(); $course = $page->course()->whereSlug($course_slug)->first(); abort_if(! isset($page) || !isset($course), 404); return $page; }} 应该做什么。它应该修改表达式。

然而,他们的例子非常不现实,所以我想知道为什么需要它呢?你能说出一些真实世界的案例吗?修改表达式树会有意义吗?或者,为什么必须进行修改?从什么到什么?

它还有许多重载用于访问各种表达式。我如何知道何时应该使用它们以及它们应该返回什么?我看到有人使用ExpressionVisitor并返回VisitParameter另一方面正在返回base.VisitParameter(node)

4 个答案:

答案 0 :(得分:7)

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

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

所以我们使用表达式访问者在将树发送到EF之前将所有属性访问权限更改为.Where(x =&gt; x.Property)到.Where(x =&gt; x.Property.Value == 1)

答案 1 :(得分:6)

  

你能说出一些真实世界的案例吗?修改表达式树会有意义吗?

严格地说,我们从不修改表达式树,因为它们是不可变的(从外部看,至少,它没有承诺它不会内部记忆值或者具有可变的私有状态)。这正是因为它们是不可变的,因此我们不能只改变一个节点,如果我们想要创建一个基于我们拥有但不同的新表达式树,那么访问者模式很有意义。以某种特定的方式(我们最接近修改不可变对象的东西)。

我们可以在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反过来将返回与新子节点相同类型的新节点,或者如果子节点未被更改,则返回相同的节点。
  3. 因此,如果您不知道您需要明确地在节点上操作,无论您的目的是什么,那么您可能不需要更改它。这也意味着Update是获取部分更改的新版本节点的便捷方式。但是,无论你的目的是什么&#34;手段当然取决于用例。最常见的情况可能是一个极端或另一个极端,只有一个或两个表达类型需要覆盖,或者全部或几乎全部需要它。

    (有一点需要注意,如果您正在检查ReadOnlyCollection中包含子项的节点的子节点,例如BlockExpression的子节点的步骤和变量,或者TryExpression的节点块的子节点,你有时只会更换那些孩子,如果你没有改变,你最好自己检查一下这个缺陷[最近已修复,但尚未发布任何版本]意味着如果你将同一个孩子传给Update在与原始ReadOnlyCollection不同的集合中,然后会不必要地创建一个新表达式,这会对树有更大的影响。这通常是无害的,但它浪费时间和内存。)

答案 2 :(得分:2)

ExpressionVisitor启用了Expression的{​​{3}}。

从概念上讲,问题是当您导航Expression树时,您所知道的是任何给定节点都是Expression,但您并不知道具体是哪种Expression 1}}。通过此模式,您可以了解您正在使用的Expression类型,并指定针对不同类型的特定类型处理。

如果您有Expression,则可以致电.ModifyExpression知道自己的类型,因此它会回拨相应的override

查看visitor pattern

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。您还可以指定其他override方法来处理其他类型的Expression

值得注意的是,由于这是一个过载的分辨率技巧,所以访问Expression会回调最合适的方法。因此,如果有不同类型的BinaryExpression,那么您可以为一个特定的子类型编写override;如果另一个子类型回调,它只会使用默认的BinaryExpression处理。

简而言之,此模式允许您浏览Expression树,知道您正在使用哪种Expression

答案 3 :(得分:2)

我转移到EF Core并从Sql Server(MS Specific)迁移到SqlLite(独立于平台)时遇到的特定现实示例。

现有的业务逻辑围绕着一个中间层/服务层接口,假设全文搜索(FTS)在后台自动神奇地发生,它与SQL Server一样。通过Expressions和FTS将搜索相关查询传递到此层,而Sql Server存储不需要额外的FTS特定实体。

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

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

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

因此,ExpressionVisitor非常有用,特别是当与能够通过各种形式的IPC传递表达式树作为数据的整个概念时。