开发者

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
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜