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