从 Action Delegate 获取实际参数类型

Get Actual Parameter Types From Action Delegate

上下文

这是一个非常具体的问题。总而言之,我有一个 class 稍后将代表排队到 运行。我知道每个方法只有一个参数,所以我将它们存储在一个 Action(Object) 列表中。我不能使用泛型,因为不同的方法可以有不同类型的参数,所以我用 Object 代替。

当委托执行时,用户传递一个参数,然后该参数将传递给委托方法。问题在于类型检查:如果他们提供的参数类型与委托人期望的类型不匹配,我不想抛出特定异常。

例如,如果将此方法传递到我的 class:

Sub Test(SomeNumber As Integer)

然后尝试 运行:

MyClass.ExecuteDelegate("DelegateArgument")

我希望能够抛出某个异常,说明他们试图将字符串用作整数并包含一些自定义信息。问题是我找不到这样做的方法。

问题

因为我将委托存储为 Action(Object) 我不知道参数的实际类型是什么。我还没有找到找到此信息的任何方法,因此我无法将参数类型与用户提供的参数类型进行比较。当我使用 Action(Object).Method.GetParameters 它只有 returns 对象

或者,当我尝试调用 Action(Object).Invoke() 时,我尝试使用 Try-Catch 块来检查 InvalidCastException,但这也会捕获委托方法中的任何 InvalidCastException,这是我不想要的。

有什么方法可以实现我想要做的事情吗?

引入一个 class,其中包含您的方法的信息,并将其用于存储委托。

Public Class MethodData
    Public ReadOnly Property ArgumentType As Type
    Public ReadOnly Property Method As Action(Of Object)

    Public Sub New(argumentType As Type, method As Action(Of Object))
        ArgumentType = argumentType
        Method = method
    End Sub
End Class

Public Sub MethodWithInteger(argument As Integer)

End Sub

Public Sub MethodWithString(argument As String)

End Sub

Dim myDelegates = New List(Of MethodData) From
{
    New MethodData(GetType(Integer), AddressOf MethodWithInteger),
    New MethodData(GetType(String), AddressOf MethodWithString)
}

'execute method
Dim inputArgument = "notInteger"
Dim methodWithInteger = myDelegates(0)
If methodData.ArgumentType Is inputArgument.GetType() Then
    methodData.Method.Invoke(inputArgument)
End If

您可以将执行逻辑放在 class MethodData.

Public Class MethodData
    Public ReadOnly Property ArgumentType As Type
    Public ReadOnly Property Method As Action(Of Object)

    Public Sub New(argumentType As Type, method As Action(Of Object))
        ArgumentType = argumentType
        Method = method
    End Sub

    Public Sub Execute(input As Object)
        If ArgumentType Is input.GetType() Then
            Method.Invoke(input)
        End If
    End Sub
End Class

请注意上面的代码(显然也是您的代码)只有在 Option Strict 设置为 Off 时才会编译。如果你将它设置为 On,你将被迫将你的方法强制转换或包装为 Action(Of Object).

我不同意你不能使用泛型。在下面显示的示例中,我已将队列定义为字典以允许基于 ID 进行检索。我这样做主要是因为我不明白您是如何设想检索给定操作的。

队列本身将 Actions 存储为委托,class 提供添加和 运行 Action 的方法。 Action的参数类型信息通过Reflection获取。

Public Class DelegateQueue
    Private queue As New Dictionary(Of String, [Delegate])

    Public Sub AddMethod(Of T)(id As String, action As Action(Of T))
        queue.Add(id, action)
    End Sub

    Public Sub RunMethod(Of T)(id As String, arg As T)
        Dim meth As [Delegate] = Nothing
        If queue.TryGetValue(id, meth) Then
            Dim parameters As Reflection.ParameterInfo() = meth.Method.GetParameters
            Dim passedType As Type = GetType(T)
            If parameters.Length > 0 AndAlso parameters(0).ParameterType Is passedType Then
                Dim act As Action(Of T) = CType(meth, Global.System.Action(Of T))
                act.Invoke(arg)
            Else
                Throw New ArgumentException(String.Format("passed argument of type {0} to Action(Of {1}). Method ID {2}", passedType.Name, parameters(0).ParameterType.Name, id))
            End If
        Else
            Throw New ArgumentException("Method ID "" " & id & """ not found.")
        End If
    End Sub
End Class

用法示例:

Sub Demo()
    Dim q As New DelegateQueue
    q.AddMethod(Of Int32)("S1", AddressOf S1)
    q.AddMethod(Of String)("S2", AddressOf S2)

    q.RunMethod("S1", 23)
    q.RunMethod("S1", "fred") ' throws runtime error
End Sub

Private Sub S1(i As Int32)
End Sub

Private Sub S2(i As String)
End Sub

我最终(在大量谷歌搜索之后)找到了另一个角度并拼凑出一个我满意的解决方案。

我偶然发现了 this,这本身并不是一个真正的答案(并且有很多我无法阅读的 C#,更不用说翻译成 VB),但它确实给了我一个想法。结局是这样的:

我对 "register" 的原始子方法与我的 class 看起来(简化)如下:

RegisterOperation(Routine As Action(Of Object))

它的名字是这样的:

RegisterOperation(AddressOf RoutineMethod)

我现在用这样的超载超载:

RegisterOperation(Routine As MethodInfo, Optional Instance As Object = Nothing)

也可以这样称呼:

RegisterOperation([GetType].GetMethod(NameOf(Routine))) 'For shared methods
RegisterOperation([GetType].GetMethod(NameOf(Routine)), Me) 'For instance methods

当然,它不如 AddressOf 漂亮,但也不会太粗鲁,而且效果很好。

我将 MethodInfo 转换为这样的委托:

Dim ParamTypes = (From P In Routine.GetParameters() Select P.ParameterType)
Routine.CreateDelegate(Expression.GetDelegateType(ParamTypes.Concat({GetType(Void)}).ToArray), Instance)

这创建了一个保留所有原始参数数据完整的委托,并允许用户注册一个方法而不必在多个地方声明参数类型,同时仍然允许我访问方法的数量和类型参数,以便我可以验证它们。

此外,我必须感谢@TnTinMn,因为他向我展示了我可以将我的代表存储为 [Delegate],而不是将其硬编码为特定的 Action(Of T)。这使我能够将所有这些不同类型的委托存储在一起,并保留我的原始实现以实现向后兼容性。只要我确保参数数量和类型正确,一切都会顺利进行。

编辑:

在使用了一下之后,我找到了一种进一步简化它的方法。如果用户 "registering" 的方法是在调用 RegisterOperation 的 class 中声明的,那么我可以使用 StackTrace 来查找调用类型,如 here.

所示

我在另一个重载中实现了这个,这样用户只需像这样传递方法的名称:

RegisterOperation(NameOf(Routine))

'To get the method from the name:
Dim Routine = (New StackTrace).GetFrame(1).GetMethod.ReflectedType.GetMethod(RoutineName)

这使得它和 AddressOf 一样干净!