Exception when building LINQ query with multiple levels of delegates or lambdas ("Value cannot be null. Parameter name: instance")
EDIT
I'm getting an exception any time I try to build a LINQ expression out of a method that contains a call to a locally defined delegate (or lambda) or a function in the class or module.
I've got a function that takes two parameters (a delegate and an integer) and then creates a LINQ MethodCallExpression
, which it uses to gets the results:
Public Delegate Function CompareTwoIntegerFunction(ByVal i1 As Integer, ByVal i2 As Integer) As Boolean
Public Function Test(ByVal pFunc As CompareTwoIntegerFunction, ByVal i1 As Integer, ByVal i2 As Integer) As Boolean
Dim lParamExpression As ParameterExpression = Expression.Parameter(GetType(Integer), "i")
Dim lConstExpr As ConstantExpression = Expression.Constant(i1, GetType(Integer))
Dim lMatcher As CompareTwoIntegerFunction = pFunc
' *** This line throws the exception (line 17)
Dim lMatcherExpr As MethodCallExpression = Expression.Call(lMatcher.Method, lParamExpression, lConstExpr)
' Now use the expression and get the result
Dim lFunc As Func(Of Integer, Boolean) = (Expression.Lambda(Of Func(Of Integer, Boolean))(lMatcherExpr, lParamExpression)).Compile
Return lFunc(i2)
End Function
I can run this code without error in this test:
Dim lMatchedPass1 As Boolean = Test(Function(a, b) a = b, 10, 10)
I can even get fancy and do something like this:
Dim lMatchedPass2 As Boolean = Test(Function(a, b) (Function(c As Integer) c + 1)(a) = b, 10, 9)
However, if I try something like this, then I get an exception:
Dim ChildFunc As Func(Of Integer, Integer) = Function(s As Integer) s + 1
Dim MatchedFail1 As Boolean = Test(Function(a, b) (ChildFunc(a)) = b, 10, 9)
Exception message:
Exception: [2011 Jun 23 (Thu) 10:25:29 AM] [ System.Core ]
Value cannot be null.
Parameter name: instance
at System.Linq.Expressions.Expression.ValidateCallArgs(Expression instance, MethodInfo method, ReadOnlyCollection`1& arguments)
at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, IEnumerable`1 arguments)
at System.Linq.Expressions.Expression.Call(MethodInfo method, Expression[] arguments)
at DataManager`1.Test(CompareTwoIntegerFunction pFunc, Int32 i1, Int32 i2) in DataManager.vb:line 17
at DataManager`1..ctor() in DataManager.vb:line 27
Called from: Void ValidateCallArgs(System.Linq.Expressions.Expression, System.Reflection.MethodInfo, System.Collections.ObjectModel.ReadOnlyCollection`1[System.Linq.Expressions.Expression] ByRef)
I want to understand why MatchTest2
is okay but MatchFail1
is a problem.
Note 1: The example above uses a constant and a parameter but I get the same results with two constants or two parameters. I can also get the exception by replacing the named delegate type with an anonymous function definition.
Note 2:
I've tried, witho开发者_高级运维ut success, adding Nothing
as the instance parameter to the Expression.Call
line, like this:
Dim lMatcherExpr As MethodCallExpression = Expression.Call(Nothing, pFunc.Method, lParamExpression, lConstExpr)
Note 3: I've found that I can "fix" the problem in the above code by moving my "child function" out into a module or by making it into a "shared" function in my class. This isn't a very realistic solution though in the non-trivial case because I want to be able to pass the child function as a parameter.
In other words, my goal is to do something like this:
Public Function RunTestWithChildFunction(ByVal a As Integer, ByVal b As Integer, ByVal ChildFunc As Func(Of Integer, Integer, Boolean)) As Boolean
Return Test(Function(x, y) ChildFunc(x, y), a, b)
End Function
Public Function EqualityCheck(ByVal a As Integer, ByVal b As Integer) As Boolean
Return a = b
End Function
Public Sub DoTest
Dim lMatchedFail2 As Boolean = RunTestWithChildFunction(10, 10, AddressOf EqualityCheck)
End
Adding "shared" to EqualityCheck
in this code does not solve the problem because the address is passed ByVal
into RunCheckWithChildFunction
(I get a compile failure if I try to pass it ByRef
).
It seems that I can work around the problem by introducing an intermediate class. The class instance holds the address to the "inner" lambda functions.
So, instead of this:
Dim lMatchedPass2 As Boolean = Test(Function(a As Integer, b As Integer) (Function(c As Integer) c + 1)(a) = (b + 1), 10, 10)
Dim ChildFunc As Func(Of Integer, Integer) = Function(s As Integer) s + 1
Dim MatchedFail1 As Boolean = Test(Function(a, b) (ChildFunc(a)) = b, 10, 9)
I would have calls like this:
Dim lDoCompare As New DoCompare(Of Integer)(10)
lDoCompare.Comparator = Function(a As Integer, b As Integer) (Function(c As Integer) c + 1)(a) = (b + 1)
Dim lMatchClassPass1 as Boolean = TestClass(lDoCompare, 10)
Dim ChildFunc As Func(Of Integer, Integer) = Function(c As Integer) c + 1
lDoCompare.Comparator = Function(a As Integer, b As Integer) ChildFunc(a) = (b + 1)
Dim lMatchClassPass2 as Boolean = TestClass(lDoCompare, 10)
Where the new classes look like this:
Public Function TestClass(Of T)(ByVal pComparator As IDoCompare(Of T), ByVal pValue As T) As Boolean
Dim lParamExpression As ParameterExpression = Expression.Parameter(GetType(T), "t")
Dim lCompareFunc As IDoCompare(Of T).Compare = AddressOf pComparator.DoCompare
Dim lInstance As ConstantExpression = Expression.Constant(pComparator)
Dim lMatcherExpr As MethodCallExpression = Expression.Call(lInstance, lCompareFunc.Method, lParamExpression)
Dim lFunc As Func(Of T, Boolean) = (Expression.Lambda(Of Func(Of T, Boolean))(lMatcherExpr, lParamExpression)).Compile
Return lFunc(pValue)
End Function
Interface IDoCompare(Of T)
Delegate Function Compare(ByVal pParam As T) As Boolean
Function DoCompare(ByVal pParam As T) As Boolean
End Interface
Class DoCompare(Of T) : Implements IDoCompare(Of T)
Protected mClue As T
Sub New(ByVal pClue As T)
mClue = pClue
End Sub
Public Comparator As Func(Of T, T, Boolean) = Function(a As T, B As T) a.ToString = B.ToString
Public Function DoCompare(ByVal pParam As T) As Boolean Implements IDoCompare(Of T).DoCompare
Return Comparator(mClue, pParam)
End Function
End Class
精彩评论