从MethodInfo创建委托,该委托具有比委托签名

时间:2018-04-07 18:12:08

标签: c# reflection types delegates polymorphism

当目标方法参数是从委托签名中指定的参数类型派生的类型时,我无法弄清楚如何创建委托。这甚至可能吗?我的项目涉及为许多不同的命令创建命令处理程序。这些命令按组排列,每个组都有自己的属性和依赖关系(为简洁起见,我在此省略了这些命令。)我试图从它们中提取那些处理程序方法(标有' Handler'属性) group,从生成的MethodInfo创建委托,并将它们存储在Dictionary中,键入命令类型。粗略的想法如下:

public delegate void HandlerDelegate(ICommand command);

public class Handler: Attribute
{
}

public interface ICommand
{
}

public class CommandA: ICommand
{
    public string CmdASpecific = "Command A";
}

public class CommandB: ICommand
{
    public string CmdBSpecific = "Command B";
}

public class HandlerGroup
{
    [Handler]
    public void HandleA(CommandA command)
    {
        Console.WriteLine($"{command.CmdASpecific} Handled");
    }

    [Handler]
    public void HandleB(CommandB command)
    {
        Console.WriteLine($"{command.CmdBSpecific} Handled");
    }
}

我使用反射扫描HandlerGroup实例,使用' Handler'提取方法。属性,从它们创建一个委托,并将它们添加到一个字典(由处理程序期望的参数类型键入)以便稍后调用:

public void Main(){
        var handlerGroup = new HandlerGroup();

        Dictionary<Type, HandlerDelegate> cache = new Dictionary<Type, HandlerDelegate>();

        foreach (var handlerInfo in handlerGroup.GetType().GetMethods())
        {
            if ((Handler)handlerInfo.GetCustomAttribute(typeof(Handler), false) is Handler handlerAttribute)
            {
                var parameters = handlerInfo.GetParameters();
                HandlerDelegate handler = (HandlerDelegate)Delegate.CreateDelegate(typeof(HandlerDelegate), handlerGroup, handlerInfo.Name);
                cache.Add(parameters.Single().ParameterType, handler);
            }
        }

        var cmdA = new CommandA();
        var cmdB = new CommandB();

        cache[cmdA.GetType()](cmdA); //Should write 'Command A Handled'
        cache[cmdB.GetType()](cmdB); //Should write 'Command B Handled'
}

CreateDelegate方法因System.ArgumentException而失败:&#39;无法绑定到目标方法,因为其签名或安全透明度与委托类型的签名或安全透明度不兼容。&#39;异常。

我可以通过向Handler属性添加属性来解决这个问题:

public class Handler: Attribute
{
    public Type CommandType;
    public Handler(Type commandType)
    {
        CommandType = commandType;
    }
}

使用此HandlerGroup代替:

public class HandlerGroup
{
    [Handler(typeof(CommandA))]
    public void HandleA(ICommand command)
    {
        var tmp = (CommandA)command;
        Console.WriteLine($"{tmp.CmdASpecific} Handled");
    }

    [Handler(typeof(CommandB))]
    public void HandleB(ICommand command)
    {
        var tmp = (CommandB)command;
        Console.WriteLine($"{tmp.CmdBSpecific} Handled");
    }
}

然后使用Handler属性的CommandType属性将其添加到缓存中,而不是使用处理程序方法&#39;参数类型:

            if (handlerInfo.GetCustomAttribute(typeof(Handler), false) is Handler handlerAttribute)
            {
                HandlerDelegate handler = (HandlerDelegate)Delegate.CreateDelegate(typeof(HandlerDelegate), handlerGroup, handlerInfo.Name);
                cache.Add(handlerAttribute.CommandType, handler);
            }

还有其他选择吗?虽然这有效,但我真的不想依赖假设处理程序以某种方式实现。

2 个答案:

答案 0 :(得分:1)

Action<CommandA>投射到Action<ICommand>不是类型安全的,因为前者只接受CommandA,而后者只接受任何ICommand,例如CommandB。出于这个原因

Delegate.CreateDelegate(typeof(HandlerDelegate), ...)

同样失败,因为HandleA(和HandleB)的签名与HandlerDelegate不兼容。

解决它的一种方法是构造具有正确类型的委托,并使用DynamicInvoke动态调用它,例如:

var parameters = handlerInfo.GetParameters();
//                                           constructing correct delegate here
var dynamicHandler = Delegate.CreateDelegate(typeof(Action<>).MakeGenericType(parameters.Single().ParameterType), handlerGroup, handlerInfo);
HandlerDelegate handler = (p) =>
{
    // invoking
    dynamicHandler.DynamicInvoke(p);
};
cache.Add(parameters.Single().ParameterType, handler);

但更好的方法是创建表达式树并将其编译为委托:

var parameters = handlerInfo.GetParameters();
// expression of type ICommand
var expressionArg = Expression.Parameter(typeof(ICommand), "x");
// this is handlerInfo.HandleA((CommandA) x)
var callExp = Expression.Call(Expression.Constant(handlerGroup), handlerInfo, Expression.Convert(expressionArg, parameters.Single().ParameterType));
// this is delegate x => handlerInfo.HandleA((CommandA) x)
var handler = Expression.Lambda<HandlerDelegate>(callExp, new[] { expressionArg }).Compile();                    
cache.Add(parameters.Single().ParameterType, handler);

答案 1 :(得分:0)

我会单独封装你的缓存;您可以通过方法组添加特定的委托。例如:

public class HandlerCache
{
    private readonly Dictionary<Type, HandlerDelegate> handlers = new
        Dictionary<Type, HandlerDelegate>();

    public void AddHandler<T>(Action<T> action) where T : ICommand =>
        handlers.Add(typeof(T), command => action((T) command);

    public void HandleCommand(ICommand command)
    {
        var commandType = command.GetType();
        if (!handlers.TryGetValue(commandType, out var handler))
        {
            throw new ArgumentException($"Unable to handle commands of type {commandType}");
        }
        handler(command);
    }
}

然后您可以像这样使用它:

var cache = new HandlerCache();
// This bit could be done by reflection if you want
var handlerGroup = new HandlerGroup();
cache.AddHandler(handlerGroup.HandleA);
cache.AddHandler(handlerGroup.HandleB);
HandlerDelegate handler = cache.HandleCommand;
// Use handler however you want

显然,总会有错误的余地 - 总会有可能会传递你无法处理的命令。请注意,这当前不处理继承,因为如果有一个派生自CommandA的类,则HandleCommand中不会匹配;如果你想找到最具体的可用处理程序,你可能会通过基类型等。

如果通过反射将处理程序添加到缓存中,这种方法意味着您可以在不添加任何属性的情况下确定类型 - 您只需要确定适当的参数类型并将方法添加到缓存中。和往常一样,反射代码写起来有点烦人,但至少你可以用最少的努力添加新的处理程序。