Получить фактические типы параметров из делегата действия
контекст
Вот очень специфическая проблема для тебя. Подводя итог, у меня есть класс, который очереди делегатов для запуска позже. Я знаю, что у каждого метода будет только один аргумент, поэтому я сохраняю их в списке Action(Object). Я не могу использовать дженерики, потому что разные методы могут иметь параметры разных типов, поэтому я использую Object.
Когда приходит время выполнить делегат, пользователь передает аргумент, который затем передается методу делегата. Проблема в проверке типов: я не хочу выдавать конкретное исключение, если тип предоставленного параметра не соответствует типу, который ожидает делегат.
Например, если передать этот метод в мой класс:
Sub Test(SomeNumber As Integer)
а затем попробуйте запустить:
MyClass.ExecuteDelegate("DelegateArgument")
Я хочу иметь возможность выдавать определенное исключение, говоря, что они пытались использовать строку как целое число и включать некоторую пользовательскую информацию. Проблема в том, что я не могу найти способ сделать это.
проблема
Поскольку я храню делегаты как Action (Object), я понятия не имею, каков действительный тип параметра. Я не нашел способа найти эту информацию, поэтому не могу сравнить тип параметра с типом предоставленного пользователем аргумента. Когда я использую Action(Object).Method.GetParameters
он только возвращает объект
В качестве альтернативы я попытался использовать блок Try-Catch для проверки исключения InvalidCastException при попытке вызвать Action(Object).Invoke()
но это также ловит любое InvalidCastException в методе делегата, который я не хочу.
Есть ли способ добиться того, что я пытаюсь сделать?
3 ответа
В итоге (после большого количества поисков в Google) я нашел другой подход и нашел решение, которым я доволен.
Я наткнулся на это, что само по себе не было ответом (и у меня было много C#, который я не мог прочитать, не говоря уже о том, чтобы перевести на VB), но это действительно дало мне идею. Вот как это закончилось:
Моя оригинальная подпрограмма для "регистрации" метода с моим классом выглядела (упрощенно) так:
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], а не жестко кодировать его как конкретное действие (Of T). Это позволило мне хранить все эти различные типы делегатов вместе, а также оставить мою первоначальную реализацию на месте для обратной совместимости. Пока я гарантирую, что номер параметра и типы верны, все идет гладко.
РЕДАКТИРОВАТЬ:
Пользуясь этим немного, я нашел способ еще больше упростить его. Если метод, который пользователь "регистрирует", объявлен в классе, который вызывает RegisterOperation, то я могу использовать StackTrace, чтобы найти тип вызова, как показано здесь.
Я реализовал это в другой перегрузке, так что пользователям нужно только передать имя метода следующим образом:
RegisterOperation(NameOf(Routine))
'To get the method from the name:
Dim Routine = (New StackTrace).GetFrame(1).GetMethod.ReflectedType.GetMethod(RoutineName)
Это делает его таким же чистым, как AddressOf!
Представьте класс, который содержит информацию о вашем методе и используйте ее для хранения делегатов.
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
Вы можете поставить логику выполнения в классе 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)
,
Я не согласен, что вы не можете использовать дженерики. В примере, показанном ниже, я определил очередь как словарь, чтобы разрешить поиск по идентификатору. Я сделал это главным образом, так как я не понимаю, как вы предполагали получить данное Действие.
Сама очередь хранит действия в качестве делегата, а класс предоставляет методы для добавления и запуска действия. Информация о типе аргумента 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