2

我今天在动态表达式构建库中实现一个功能时遇到了一个有趣的问题。更具体地说,但无关紧要的是,在表达式中定义运算符优先级的功能。

当 LINQ 引擎编译最终表达式时,我遇到了一个InvalidOperationException声明Lambda parameter out of scope.

ParameterExpression分配相关对象后,问题就显现出来了。

在使用完整且格式良好的 lambda 表达式树时,我发现ParameterExpression在编译 Lambda 时将 Lambda 的对象重新分配给有效引用是无效的。

这是我在应用修复之前最初采用的行为的简短描述:

  • 构建表达式树,用于使用 with Queryable.Where,根表达式为 a LambdaExpression,构造使用Expression.Lambda(expression, Expression.Parameter(GetType(type), "name"))
  • 访问表达式树(使用LinqKit),构建遇到的参数哈希表
  • 随后的同名参数被替换为遇到的第一个同名参数

结果是一个表达式树,其中所有ParameterExpression同名的引用都指向同一个对象 - 但InvalidOperationException在编译时遇到了。

我应用的修复采用了以下行为:

  • 将参数构建为数组ParameterExpression
  • 构造根 Lambda,使用Expression.Lambda(expression, parameterArray)
  • 访问表达式树(使用 LinqKit),将遇到的参数替换为参数 fromparameterArray

最终结果编译得很好,即使 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

4

1 回答 1

3

AFAIK 表达式树不会将使用相同参数创建的两个 ParameterExpression 对象视为“相同参数”。

那么,在没有测试您的代码的情况下,这就是突出的:当我阅读第一个(失败)场景时,您将所有同名参数替换为遇到的第一个此类参数,但第一个遇到的参数与第一个参数不是同一个 ParameterExpression对象您在对 Expression.Lambda() 的调用中创建。在第二个(成功的)场景中,它是。

已编辑我应该补充一点,我没有使用过 LinqKit 的 ExpressionVisitor,但据我所知,它是基于我使用过的代码,其中 VisitLambda 不是很健壮:

    protected virtual Expression VisitLambda(LambdaExpression lambda)
    {
        Expression body = this.Visit(lambda.Body);
        if (body != lambda.Body)
        {
            return Expression.Lambda(lambda.Type, body, lambda.Parameters);
        }
        return lambda;
    }

请注意,访问的是表达式的主体,而不是其参数。如果 LinqKit 没有改进这一点,那将是失败的地方。

于 2009-07-16T18:40:11.447 回答