2

今日、動的式構築ライブラリに機能を実装しているときに、興味深い問題に遭遇しました。より具体的には、無関係ですが、式で演算子の優先順位を定義する機能。

LINQエンジンが最終式をコンパイルしているときに、InvalidOperationException宣言に遭遇しましたLambda parameter out of scope

ParameterExpression問題は、関連するオブジェクトを割り当てた後に明らかになります。

完全で整形式のラムダ式ツリーを使用してParameterExpression、Lambdaをコンパイルするときに、Lambdaのオブジェクトを有効な参照に再割り当てすることは無効であることがわかりました。

これは、修正を適用する前に最初に採用した動作の簡単な説明です。

  • で使用することを目的とした式ツリーを構築しますQueryable.Where。ルート式は、を使用してLambdaExpression構築されます。Expression.Lambda(expression, Expression.Parameter(GetType(type), "name"))
  • (LinqKitを使用して)式ツリーにアクセスし、検出されたパラメーターのハッシュテーブルを作成します
  • 同じ名前の後続のパラメーターは、同じ名前の最初に検出されたパラメーターに置き換えられます

ParameterExpression結果は、同じ名前のすべての参照がすべて同じオブジェクトを指している式ツリーでしたInvalidOperationExceptionが、コンパイル時に検出されました。

私が適用した修正では、次の動作が採用されました。

  • パラメータをの配列として構築しますParameterExpression
  • を使用してルートラムダを構築しますExpression.Lambda(expression, parameterArray)
  • (LinqKitを使用して)式ツリーにアクセスし、次のパラメーターで検出されたパラメーターを置き換えます。parameterArray

Lambda式の構造は概念的には前の動作からの出力と同じですが、最終結果は正常にコンパイルされます。

問題は、最初はなぜ失敗し、2番目は成功するのかということです。

以下は、テストケースといくつかのサポートクラス(nUnit、LinqKitに依存)を含む、再現するテストフィクスチャクラス(vbを言い訳)です。

注:TestFixtureとTestの属性宣言がありません-マークダウンで行う方法???



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式ツリーは、同一の引数で作成された2つのParameterExpressionオブジェクトを「同じパラメーター」とは見なしません。

コードをテストしていなければ、それが際立っています。最初の(失敗した)シナリオを読んだときに、同じ名前のパラメーターをすべて最初に検出されたものに置き換えますが、最初に検出されたパラメーターは、そのパラメーターと同じParameterExpressionオブジェクトではありません。 Expression.Lambda()の呼び出しで作成します。2番目の(後続の)シナリオでは、そうです。

編集済み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 に答える