Сегодня я столкнулся с интересной проблемой при реализации функции в библиотеку создания динамических выражений. Более конкретно, но неважно, возможность определять приоритет оператора в выражении.
Когда движок 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