我为什么要使用 ExpressionVisitor?
Why would I want to use an ExpressionVisitor?
我从有关 How to: Modify Expression Trees 的 MSDN 文章中了解到 ExpressionVisitor
应该做什么。它应该修改表达式。
然而,他们的示例非常不切实际,所以我想知道我为什么需要它?你能举出一些修改表达式树有意义的真实案例吗?或者,为什么必须修改它?从什么到什么?
它还有很多访问各种表达式的重载。我怎么知道什么时候应该使用它们中的任何一个以及它们应该 return?我看到人们使用 VisitParameter
和 returning base.VisitParameter(node)
另一方面 returning Expression.Parameter(..)
.
存在一个问题,我们在数据库中有包含 0 或 1(数字)的字段,而我们想在应用程序中使用布尔值。
解决方案是创建一个 "Flag" 对象,其中包含 0 或 1 并转换为 bool。我们在所有应用程序中都像布尔一样使用它,但是当我们在 .Where() 子句中使用它时,EntityFramework 抱怨它无法调用转换方法。
所以我们使用表达式 visitor 将所有 属性 访问更改为 .Where(x => x.Property) 到 .Where(x => x.Property.Value == 1 ) 就在将树发送到 EF 之前。
Expression
的 ExpressionVisitor
enables the visitor pattern。
从概念上讲,问题是当您导航一棵 Expression
树时,您所知道的是任何给定节点都是 Expression
,但您不知道具体是哪种 Expression
。此模式允许您了解您正在使用的 Expression
类型,并指定 type-specific 处理不同类型。
当你有 Expression
时,你可以调用 .Modify
。 Expression
知道自己的类型,所以它会回调适当的 override
.
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
的会回调best-fitting的方法。所以,如果有不同种类的 BinaryExpression
,那么你可以为一个特定的子类型写一个 override
;如果另一个子类型回调,它将只使用默认的 BinaryExpression
处理。
简而言之,此模式允许您在 Expression
树中导航,了解您正在使用哪种 Expression
。
Could you name some real-world cases where it would make sense to modify an expression tree?
严格来说,我们从不修改表达式树,因为它们是不可变的(至少从外部看,不能保证它不会在内部记忆值或以其他方式具有可变的私有状态)。正是因为它们是不可变的,因此我们不能只改变一个节点,如果我们想创建一个新的表达式树,它基于我们已有的但在某些特定方面有所不同(我们必须修改不可变的最接近的东西 object).
我们可以在 Linq 中找到一些。
在许多方面,最简单的 Linq 提供程序是 linq-to-objects 提供程序,它在内存中处理可枚举的 objects。
当它以 IEnumerable<T>
objects 的形式直接接收枚举时,它非常漂亮 straight-forward 因为大多数程序员可以很快地编写大多数方法的未优化版本。例如。 Where
只是:
foreach (T item in source)
if (pred(item))
yield return item;
等等。但是 EnumerableQueryable
实施 IQueryable<T>
版本呢?由于 EnumerableQueryable
包装了一个 IEnumerable<T>
我们可以对涉及的一个或多个可枚举 object 执行所需的操作,但是我们有一个表达式根据 IQueryable<T>
描述该操作以及选择器、谓词等的其他表达式,我们需要的是根据 IEnumerable<T>
和选择器、谓词等的委托
对该操作的描述
System.Linq.EnumerableRewriter
是 ExpressionVisitor
的一个实现,正是执行这样的 re-write,然后可以简单地编译和执行结果。
在 System.Linq.Expressions
内部有一些 ExpressionVisitor
的实现用于不同的目的。一个例子是编译的解释器形式无法直接处理引用表达式中的提升变量,因此它使用访问者将其重写为处理字典中的索引。
除了生成另一个表达式,ExpressionVisitor
还可以生成另一个结果。 System.Linq.Expressions
本身也有内部示例,带有调试字符串和 ToString()
用于通过访问相关表达式来工作的许多表达式类型。
这可以(尽管不一定是)是 database-querying linq 提供程序将表达式转换为 SQL 查询的方法。
How do I know when I should use any of them and what should they return?
这些方法的默认实现将:
- 如果表达式可以没有 child 表达式(例如
Expression.Constant()
的结果),那么它将 return 节点再次返回。
- 否则访问所有 child 表达式,然后对有问题的表达式调用
Update
,将结果传回。 Update
又会 return 一个与新的 children 相同类型的新节点,或者 return 如果 children 相同的节点再次返回没有改变。
因此,如果您不知道出于何种目的需要显式操作某个节点,那么您可能不需要更改它。这也意味着 Update
是获取节点新版本以进行部分更改的便捷方式。但是 "whatever your purposes are" 的含义当然取决于用例。最常见的情况可能会走向一个极端或另一个极端,只有一种或两种表达式类型需要覆盖,或者全部或几乎全部需要覆盖。
(一个警告是,如果您正在检查那些在 ReadOnlyCollection
中具有 children 的节点的 children,例如 BlockExpression
的步骤和变量或 TryExpression
的 catch-blocks,你有时只会更改那些 children 那么如果你没有更改,你最好自己检查一下这是一个缺陷 [最近修复,但还没有在任何发布的版本中]意味着如果你将相同的 children 传递给 Update
在与原始 ReadOnlyCollection
不同的集合中,那么会不必要地创建一个新的表达式,这会产生进一步的影响上树。这通常是无害的,但会浪费时间和内存)。
我刚刚遇到的特定现实世界示例发生在转移到 EF Core 并从 Sql 服务器(特定于 MS)迁移到 SqlLite(平台无关)时。
现有的业务逻辑围绕着一个中间层/服务层接口,假设全文搜索 (FTS) 在后台自动神奇地发生,它与 SQL 服务器一起使用。搜索相关查询通过表达式和 FTS 针对 Sql 服务器存储传递到此层,不需要额外的 FTS 特定实体。
我不想更改任何这些,但是使用 SqlLite 你必须针对特定的虚拟 table 进行全文搜索,这反过来意味着要更改所有中间内容层调用以重新定位 FTS tables/entities,然后将它们加入业务实体 table 以获得类似的结果集。
但是通过子classing ExpressionVisitor,我能够拦截 DAL 层中的调用并简单地重写传入的表达式(或者更准确地说是整个搜索表达式中的一些 BinaryExpressions)以专门处理Sql简化 FTS 要求。
这意味着数据层到数据存储的专门化发生在单个 class 中,从存储库基础 class 中的单个位置调用。无需更改应用程序的其他方面即可通过 EFCore 支持 FTS,并且任何 SqlLite FTS 相关实体都可以包含在单个可插入程序集中。
所以 ExpressionVisitor 真的非常有用,尤其是当结合了能够通过各种形式的 IPC 将表达式树作为数据传递的整个概念时。
我从有关 How to: Modify Expression Trees 的 MSDN 文章中了解到 ExpressionVisitor
应该做什么。它应该修改表达式。
然而,他们的示例非常不切实际,所以我想知道我为什么需要它?你能举出一些修改表达式树有意义的真实案例吗?或者,为什么必须修改它?从什么到什么?
它还有很多访问各种表达式的重载。我怎么知道什么时候应该使用它们中的任何一个以及它们应该 return?我看到人们使用 VisitParameter
和 returning base.VisitParameter(node)
另一方面 returning Expression.Parameter(..)
.
存在一个问题,我们在数据库中有包含 0 或 1(数字)的字段,而我们想在应用程序中使用布尔值。
解决方案是创建一个 "Flag" 对象,其中包含 0 或 1 并转换为 bool。我们在所有应用程序中都像布尔一样使用它,但是当我们在 .Where() 子句中使用它时,EntityFramework 抱怨它无法调用转换方法。
所以我们使用表达式 visitor 将所有 属性 访问更改为 .Where(x => x.Property) 到 .Where(x => x.Property.Value == 1 ) 就在将树发送到 EF 之前。
Expression
的 ExpressionVisitor
enables the visitor pattern。
从概念上讲,问题是当您导航一棵 Expression
树时,您所知道的是任何给定节点都是 Expression
,但您不知道具体是哪种 Expression
。此模式允许您了解您正在使用的 Expression
类型,并指定 type-specific 处理不同类型。
当你有 Expression
时,你可以调用 .Modify
。 Expression
知道自己的类型,所以它会回调适当的 override
.
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
的会回调best-fitting的方法。所以,如果有不同种类的 BinaryExpression
,那么你可以为一个特定的子类型写一个 override
;如果另一个子类型回调,它将只使用默认的 BinaryExpression
处理。
简而言之,此模式允许您在 Expression
树中导航,了解您正在使用哪种 Expression
。
Could you name some real-world cases where it would make sense to modify an expression tree?
严格来说,我们从不修改表达式树,因为它们是不可变的(至少从外部看,不能保证它不会在内部记忆值或以其他方式具有可变的私有状态)。正是因为它们是不可变的,因此我们不能只改变一个节点,如果我们想创建一个新的表达式树,它基于我们已有的但在某些特定方面有所不同(我们必须修改不可变的最接近的东西 object).
我们可以在 Linq 中找到一些。
在许多方面,最简单的 Linq 提供程序是 linq-to-objects 提供程序,它在内存中处理可枚举的 objects。
当它以 IEnumerable<T>
objects 的形式直接接收枚举时,它非常漂亮 straight-forward 因为大多数程序员可以很快地编写大多数方法的未优化版本。例如。 Where
只是:
foreach (T item in source)
if (pred(item))
yield return item;
等等。但是 EnumerableQueryable
实施 IQueryable<T>
版本呢?由于 EnumerableQueryable
包装了一个 IEnumerable<T>
我们可以对涉及的一个或多个可枚举 object 执行所需的操作,但是我们有一个表达式根据 IQueryable<T>
描述该操作以及选择器、谓词等的其他表达式,我们需要的是根据 IEnumerable<T>
和选择器、谓词等的委托
System.Linq.EnumerableRewriter
是 ExpressionVisitor
的一个实现,正是执行这样的 re-write,然后可以简单地编译和执行结果。
在 System.Linq.Expressions
内部有一些 ExpressionVisitor
的实现用于不同的目的。一个例子是编译的解释器形式无法直接处理引用表达式中的提升变量,因此它使用访问者将其重写为处理字典中的索引。
除了生成另一个表达式,ExpressionVisitor
还可以生成另一个结果。 System.Linq.Expressions
本身也有内部示例,带有调试字符串和 ToString()
用于通过访问相关表达式来工作的许多表达式类型。
这可以(尽管不一定是)是 database-querying linq 提供程序将表达式转换为 SQL 查询的方法。
How do I know when I should use any of them and what should they return?
这些方法的默认实现将:
- 如果表达式可以没有 child 表达式(例如
Expression.Constant()
的结果),那么它将 return 节点再次返回。 - 否则访问所有 child 表达式,然后对有问题的表达式调用
Update
,将结果传回。Update
又会 return 一个与新的 children 相同类型的新节点,或者 return 如果 children 相同的节点再次返回没有改变。
因此,如果您不知道出于何种目的需要显式操作某个节点,那么您可能不需要更改它。这也意味着 Update
是获取节点新版本以进行部分更改的便捷方式。但是 "whatever your purposes are" 的含义当然取决于用例。最常见的情况可能会走向一个极端或另一个极端,只有一种或两种表达式类型需要覆盖,或者全部或几乎全部需要覆盖。
(一个警告是,如果您正在检查那些在 ReadOnlyCollection
中具有 children 的节点的 children,例如 BlockExpression
的步骤和变量或 TryExpression
的 catch-blocks,你有时只会更改那些 children 那么如果你没有更改,你最好自己检查一下这是一个缺陷 [最近修复,但还没有在任何发布的版本中]意味着如果你将相同的 children 传递给 Update
在与原始 ReadOnlyCollection
不同的集合中,那么会不必要地创建一个新的表达式,这会产生进一步的影响上树。这通常是无害的,但会浪费时间和内存)。
我刚刚遇到的特定现实世界示例发生在转移到 EF Core 并从 Sql 服务器(特定于 MS)迁移到 SqlLite(平台无关)时。
现有的业务逻辑围绕着一个中间层/服务层接口,假设全文搜索 (FTS) 在后台自动神奇地发生,它与 SQL 服务器一起使用。搜索相关查询通过表达式和 FTS 针对 Sql 服务器存储传递到此层,不需要额外的 FTS 特定实体。
我不想更改任何这些,但是使用 SqlLite 你必须针对特定的虚拟 table 进行全文搜索,这反过来意味着要更改所有中间内容层调用以重新定位 FTS tables/entities,然后将它们加入业务实体 table 以获得类似的结果集。
但是通过子classing ExpressionVisitor,我能够拦截 DAL 层中的调用并简单地重写传入的表达式(或者更准确地说是整个搜索表达式中的一些 BinaryExpressions)以专门处理Sql简化 FTS 要求。
这意味着数据层到数据存储的专门化发生在单个 class 中,从存储库基础 class 中的单个位置调用。无需更改应用程序的其他方面即可通过 EFCore 支持 FTS,并且任何 SqlLite FTS 相关实体都可以包含在单个可插入程序集中。
所以 ExpressionVisitor 真的非常有用,尤其是当结合了能够通过各种形式的 IPC 将表达式树作为数据传递的整个概念时。