我今天在动态表达式构建库中实现一个功能时遇到了一个有趣的问题。更具体地说,但无关紧要的是,在表达式中定义运算符优先级的功能。
当 LINQ 引擎编译最终表达式时,我遇到了一个InvalidOperationException
声明Lambda parameter out of scope
.
ParameterExpression
分配相关对象后,问题就显现出来了。
在使用完整且格式良好的 lambda 表达式树时,我发现ParameterExpression
在编译 Lambda 时将 Lambda 的对象重新分配给有效引用是无效的。
这是我在应用修复之前最初采用的行为的简短描述:
- 构建表达式树,用于使用 with
Queryable.Where
,根表达式为 aLambdaExpression
,构造使用Expression.Lambda(expression, Expression.Parameter(GetType(type), "name"))
- 访问表达式树(使用LinqKit),构建遇到的参数哈希表
- 随后的同名参数被替换为遇到的第一个同名参数
结果是一个表达式树,其中所有ParameterExpression
同名的引用都指向同一个对象 - 但InvalidOperationException
在编译时遇到了。
我应用的修复采用了以下行为:
- 将参数构建为数组
ParameterExpression
- 构造根 Lambda,使用
Expression.Lambda(expression, parameterArray)
- 访问表达式树(使用 LinqKit),将遇到的参数替换为参数 from
parameterArray
最终结果编译得很好,即使 Lambda 表达式结构在概念上与前一个行为的输出相同。
问题是:为什么第一个失败,第二个成功?
下面是一个要重现的测试夹具类(请原谅 vb),带有测试用例和几个支持类(取决于 nUnit,LinqKit):
注意:缺少 TestFixture 和 Test 属性声明 - 如何在 markdown 中执行?
Imports LinqKit
Imports NUnit.Framework
Imports System.Linq.Expressions
_
Public Class ParameterOutOfScopeTests
Public Class TestObject
Public Name As String
Public DateOfBirth As DateTime = DateTime.Now
Public DateOfDeath As DateTime?
End Class
Public Class ParameterNormalisation
Inherits ExpressionVisitor
Public Sub New(ByVal expression As Expression)
_expression = expression
End Sub
Private _expression As expression
Private _parameter As ParameterExpression
Private _name As String
Public Function Normalise(ByVal parameter As ParameterExpression) As Expression
_parameter = parameter
_name = parameter.Name
_expression = Me.Visit(_expression)
Return _expression
End Function
Public Function Normalise(ByVal name As String) As Expression
_name = name
_expression = Me.Visit(_expression)
Return _expression
End Function
Protected Overrides Function VisitParameter(ByVal p As System.Linq.Expressions.ParameterExpression) As System.Linq.Expressions.Expression
Debug.WriteLine("ClientExpressionParameterNormalisation.VisitParameter:: Parameter visited: " & p.Name & " " & p.GetHashCode)
If p.Name.Equals(_name) Then
If _parameter Is Nothing Then
_parameter = p
Debug.WriteLine("ClientExpressionParameterNormalisation.VisitParameter:: Primary parameter identified: " & p.GetHashCode)
ElseIf Not p Is _parameter Then
Debug.WriteLine("ClientExpressionParameterNormalisation.VisitParameter:: Secondary parameter substituted: " & p.GetHashCode & " with " & _parameter.GetHashCode)
Return MyBase.VisitParameter(_parameter)
Else
Debug.WriteLine("ClientExpressionParameterNormalisation.VisitParameter:: Parameter already common: " & p.GetHashCode & " with " & _parameter.GetHashCode)
End If
End If
Return MyBase.VisitParameter(p)
End Function
End Class
_
Public Sub Lambda_Parameter_Out_Of_Scope_As_Expected()
Dim treeOne As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) test.DateOfBirth > Now And test.Name.Contains("name")
Dim treeTwo As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) Not test.DateOfDeath.HasValue
Dim treeThree As Expression = Expression.And(treeOne.Body, treeTwo.Body)
Dim realParameter As ParameterExpression = Expression.Parameter(GetType(TestObject), "test")
Dim lambdaOne As LambdaExpression = Expression.Lambda(treeThree, realParameter)
Dim delegateOne As [Delegate] = lambdaOne.Compile
End Sub
_
Public Sub Lambda_Compiles()
Dim treeOne As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) test.DateOfBirth > Now And test.Name.Contains("name")
Dim treeTwo As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) Not test.DateOfDeath.HasValue
Dim treeThree As Expression = Expression.And(treeOne.Body, treeTwo.Body)
Dim normaliser As New ParameterNormalisation(treeThree)
Dim realParameter As ParameterExpression = Expression.Parameter(GetType(TestObject), "test")
treeThree = normaliser.Normalise(realParameter)
Dim lambdaOne As LambdaExpression = Expression.Lambda(treeThree, realParameter)
Dim delegateOne As [Delegate] = lambdaOne.Compile
End Sub
_
Public Sub Lambda_Fails_But_Is__Conceptually__Sound()
Dim treeOne As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) test.DateOfBirth > Now And test.Name.Contains("name")
Dim treeTwo As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) Not test.DateOfDeath.HasValue
Dim treeThree As Expression = Expression.And(treeOne.Body, treeTwo.Body)
Dim realParameter As ParameterExpression = Expression.Parameter(GetType(TestObject), "test")
Dim lambdaOne As LambdaExpression = Expression.Lambda(treeThree, realParameter)
Dim normaliser As New ParameterNormalisation(lambdaOne)
lambdaOne = DirectCast(normaliser.Normalise("test"), LambdaExpression)
Dim delegateOne As [Delegate] = lambdaOne.Compile
End Sub
End Class