Компиляция делегата с помощью Expression.Lambda () - параметр выходит за рамки, но так ли это на самом деле? - PullRequest
2 голосов
/ 16 июля 2009

Сегодня я столкнулся с интересной проблемой при реализации функции в библиотеку создания динамических выражений. Более конкретно, но неважно, возможность определять приоритет оператора в выражении.

Когда движок LINQ компилировал окончательное выражение, я столкнулся с InvalidOperationException объявлением Lambda parameter out of scope.

Проблема проявляется после назначения соответствующих ParameterExpression объектов.

Работая с полным и правильно сформированным деревом лямбда-выражений, я обнаружил, что переназначение объектов ParameterExpression лямбды действительным ссылкам недопустимо при компиляции лямбды.

Это краткое описание поведения, которое я изначально использовал, прежде чем применить исправление:

  • Построить дерево выражений, предназначенное для использования с Queryable.Where, корневое выражение - LambdaExpression, построенное с использованием Expression.Lambda(<em>expression</em>, Expression.Parameter(GetType(<em>type</em>), "<em>name</em>"))
  • Посетите дерево выражений (используя LinqKit), создайте хеш-таблицу найденных параметров
  • Последующие параметры с тем же именем заменяются на первый встреченный параметр с идентичным именем

Результатом было дерево выражений, в котором все ссылки ParameterExpression с одинаковыми именами указывали на один и тот же объект, но при компиляции встретился InvalidOperationException.

Исправление, которое я применил, использовало следующее поведение:

  • Создание параметров в виде массива ParameterExpression
  • Построить корневую лямбду, используя Expression.Lambda(<em>expression</em>, <em>parameterArray</em>)
  • Посетите дерево выражений (используя LinqKit), подставьте параметры, встречающиеся с параметрами из <em>parameterArray</em>

Конечный результат компилируется нормально, даже если структура лямбда-выражения концептуально такая же , что и выходные данные из предыдущего поведения.

Вопрос в следующем: Почему первый отказывает, а второй - ?

Ниже приведен класс тестового прибора для воспроизведения (извините за vb), с тестовыми примерами и парой вспомогательных классов (зависит от nUnit, LinqKit):

примечание: объявления атрибутов 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

1 Ответ

3 голосов
/ 16 июля 2009

Деревья выражений AFAIK не рассматривают два объекта ParameterExpression, созданные с одинаковыми аргументами, как "один и тот же параметр".

Итак, не проверив ваш код, вот что выделяется: когда я читаю первый (неудачный) сценарий, вы заменяете все параметры с одинаковыми именами на первый встреченный, но этот первый встреченный параметр не является тот же объект ParameterExpression , который был создан при вызове Expression.Lambda (). Во втором (последующем) сценарии это так.

РЕДАКТИРОВАНИЕ Я должен добавить, что я не использовал ExpressionVisitor LinqKit, но насколько я знаю, он основан на коде, который я использовал, в котором 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 не улучшил это, это было бы точкой отказа.

...