Компиляция делегата с Expression.Lambda() — параметр выходит за рамки, но так ли это на самом деле?

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

Когда механизм 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

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

Возникает вопрос: Почему первый терпит неудачу, а второй удается?

Ниже приведен класс тестовых приспособлений для воспроизведения (извините за 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


person Rabid    schedule 16.07.2009    source источник


Ответы (1)


Деревья выражений 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 не улучшит это, это будет точкой отказа.

person Ben M    schedule 16.07.2009
comment
Действительно, в успешном сценарии это действительно так: параметры, используемые при создании лямбда-выражения, — это те, которые используются для подстановки всех соответствующих параметров, которые еще были в выражении, и оно компилируется. При втором посещении дерева выражений я убедился, что все выражения параметров в первом сценарии указывают на предполагаемые ссылки (включая параметры лямбда): концептуально деревья выражений одинаковы. Это почти если лямбда-выражение сохраняет внутреннюю ссылку на исходные параметры. - person Rabid; 16.07.2009
comment
Итак, в приведенном выше использовании LinqKit ExpressionVisitor правильно посещает и заменяет параметры лямбды, а не только те, которые указаны в его теле? - person Ben M; 16.07.2009
comment
На самом деле, по размышлению, я этого не проверял. Но я должен... и исправить поведение, если это так, и повторить попытку. Завтра отчитаюсь :) - person Rabid; 17.07.2009
comment
На самом деле, я только что проверил источник аля albahari.com/nutshell/LinqKitSource.zip, и да! Ты прав! Отличная находка, спасибо за ваш вклад! - person Rabid; 17.07.2009