创建动作<t>其中T是Method参数</t>的基类

时间:2014-12-09 15:27:54

标签: c# delegates

我正在尝试执行类似CommandBus的操作,其中在处理命令时应该调用方法。我把它们存放在Dictonary

private readonly ConcurrentDictionary<Type, Action<BaseCommand>> _commandHandlers = new ConcurrentDictionary<Type, Action<BaseCommand>>();

因此,当处理Command of Type StartCommand时,我会找到要从此Dictonary执行的Action。

StartCommand应该调用的方法如下所示。

public void StartCommand(StartCommand command)

StartCommand 会继承 BaseCommand

我正在尝试使用此代码填充Dictonary。

    var commands = new List<Type>();

    //-- Get all commands that is defined in assembly
    var tmpAssembly = typeof(CommandBus).Assembly;
    commands.AddRange(tmpAssembly.GetTypes().Where(t => t.BaseType == typeof(BaseCommand)));

    commands.ForEach(c =>
    {
        var methodInfo = instance.GetType().GetMethods().SingleOrDefault(m => m.GetParameters().Count() == 1
                                                                        && m.GetParameters().Any(p => p.ParameterType == c));
        if (methodInfo != null)
        {
            var action = (Action<BaseCommand>)Delegate.CreateDelegate(typeof(Action<BaseCommand>), instance, methodInfo);
            if (!_commandHandlers.TryAdd(c, action))
                throw new ArgumentException(string.Format("An CommandHandler is already registered for the command: '{0}'. Only one CommandHandler can be registered for a command", c.Name));
        }
    });

当我运行此代码时,我得到以下异常:无法绑定到目标方法,因为其签名或安全透明度与委托类型的不兼容。

这是正确的,因为我的mehtod并没有将 BaseCommand 作为参数,而是 StartCommand

但是有什么方法可以创建动作吗?我已经看过表达式的som示例,但我没有设法解决它。

提前致谢

3 个答案:

答案 0 :(得分:1)

我发现问题有点令人困惑,原因如下:

  1. 我不清楚为什么实例类型的方法,例如StartCommand本身需要一个不同的StartCommand实例传递给它。
  2. 我不清楚为什么这个事实上的接口方法的参数类型必须与声明类型相同。
  3. 我认为处理这类事情的更常用的方法是让类型实现一个接口,或者至少使用相同的方法签名,即使用基类类型作为参数类型而不是实现类的类型。

    那就是说,如果你真的想按照你描述的方式去做,那么你就可以使用Expression课程了。您可以创建一个表达式,该表达式将显式转换为您需要调用该成员的类型,以便委托实例本身可以接收基类型。

    例如:

    /// <summary>
    /// Create an Action&lt;T> delegate instance which will call the
    /// given method, using the given instance, casting the argument
    /// of type T to the actual argument type of the method.
    /// </summary>
    /// <typeparam name="T">The type for the delegate's parameter</typeparam>
    /// <param name="b">The instance of the object for the method call</param>
    /// <param name="miCommand">The method to call</param>
    /// <returns>A new Action&lt;T></returns>
    private static Action<T> CreateAction<T>(B b, MethodInfo miCommand)
    {
        // Create the parameter object for the expression, and get
        // the type needed for it
        ParameterExpression tParam = Expression.Parameter(typeof(T));
        Type parameterType = miCommand.GetParameters()[0].ParameterType;
    
        // Create an expression to cast the parameter to the correct type
        // for the call
        Expression castToType = Expression.Convert(tParam, parameterType, null);
    
        // Create the delegate itself: compile a lambda expression where
        // the lambda calls the method miCommand using the instance b and
        // passing the result of the cast expression as the argument.
        return (Action<T>)Expression.Lambda(
                Expression.Call(
                    Expression.Constant(b, b.GetType()),
                    miCommand, castToType),
                tbParam).Compile();
    }
    

    您可以这样使用:

    var action = CreateAction<BaseCommand>(instance, methodInfo);
    
    if (!_commandHandlers.TryAdd(c, action))
        throw new ArgumentException(string.Format("An CommandHandler is already registered for the command: '{0}'. Only one CommandHandler can be registered for a command", c.Name));
    

    将类型名称B替换为您实际的控制器类型。不幸的是,您的代码示例未显示instance的声明,因此我无法在此处使用真实的类型名称。

    当然,在实际调用委托时,您必须小心确保传递正确类型的实例!上面就像做一个强制转换一样,如果类型实际上不能转换为方法的参数类型,就会抛出异常,就像强制转换一样。

答案 1 :(得分:0)

根据您的输入信息,我想提出一种不同的方法。

不要试图查看方法参数来找到绑定到命令处理程序的类型,最好将不同的命令处理程序拆分到它们自己的类上。这样,您可以避免必须处理不同类型的委托的复杂性,更不用说您的代码更易读,更容易测试等。

我向您介绍的想法是使用标准方法来存储类而不是方法,以执行命令。

因此,首先我们创建一个用于我们的命令处理程序的用户属性。这样,您只需将属性添加到类中,并使用要绑定到它的命令类型。

[System.AttributeUsage(System.AttributeTargets.Class)]
public class CommandTypeAttribute : Attribute
{
    protected Type fCommandType;

    public CommandTypeAttribute(Type commandType)
    {
        fCommandType = commandType;
    }

    public Type CommandType
    {
        get { return fCommandType; }
        set { fCommandType = value; }
    }
}

我们创建两个接口,一个用于命令,一个用于命令处理程序。命令接口现在为空,但您可以使用它来公开命令处理程序的公共数据/方法。

public interface ICommandHandler
{
    void Execute(ICommand data);
}

public interface ICommand
{
    // common data for commands
}

我们为测试创建了两个命令:

public class StartCommand : ICommand
{
    //Start command data
}

public class OtherCommand : ICommand
{
    //Other command data
}

然后,我们创建我们的命令处理程序(你现在可以拥有许多你想要的,这是用于演示)。我们将只创建一个,以便能够测试当我们想要运行没有绑定命令处理程序的命令时会发生什么。我们通过属性告诉哪个命令附加到此命令处理程序,我们实现了ICommandHandler接口。

[CommandTypeAttribute(typeof(StartCommand))]
public class StartCommandHandler : ICommandHandler
{
    public StartCommandHandler()
    {
    }

    public void Execute(ICommand data)
    {
        StartCommand scData = data as StartCommand;
        if (scData == null)
            throw new ArgumentException(string.Format("Invalid command for CommandHandler 'StartCommandHandler'. Expecting 'StartCommand' but received {0}", data == null ? "null" : data.GetType().Name));

        //start command execution
    }
}

在这里,我们创建一个&#34;命令管理器&#34;类。它有两个方法,一个用于填充命令字典,另一个用于执行命令。

public class CommandManager
{
    private readonly ConcurrentDictionary<Type, ICommandHandler> fCommands = new ConcurrentDictionary<Type, ICommandHandler>();

    public void FillCommandsList(Assembly[] commandDataAssemblies, Assembly[] commandAssemblies)
    {
        var data = new List<Type>();
        foreach (Assembly assembly in commandDataAssemblies)
            data.AddRange(assembly.GetTypes().Where(t => t.GetInterfaces().Contains(typeof(ICommand))));

        var commands = new List<Type>();
        foreach (Assembly assembly in commandAssemblies)
            commands.AddRange(assembly.GetTypes().Where(t => t.GetInterfaces().Contains(typeof(ICommandHandler))));

        foreach (Type dataType in data)
        {
            foreach (Type commandType in commands)
            {
                Type commandDataType = commandType.GetCustomAttribute<CommandTypeAttribute>().CommandType;
                if (commandDataType == dataType)
                {
                    if (!fCommands.ContainsKey(dataType))
                    {
                        fCommands[dataType] = (ICommandHandler)Activator.CreateInstance(commandType);
                    }
                    else
                    {
                        throw new ArgumentException(string.Format("A command handler is already registered for the command: '{0}'. Only one command handler can be registered for a command", dataType.Name));
                    }
                }
            }
        }
    }

    public void ExecuteCommand(ICommand command)
    {
        if (command == null)
            return;

        Type commandType = command.GetType();

        if (!fCommands.ContainsKey(commandType))
            throw new ArgumentException(string.Format("Command '{0}' not found", commandType.Name));

        ICommandHandler commandHandler = fCommands[commandType];
        commandHandler.Execute(command);
    }
}

这里我放了一个课程,你可以测试这个系统。

public class TestSystem
{
    public void Run()
    {
        CommandManager cm = new CommandManager();
        cm.FillCommandsList(new Assembly[] { this.GetType().Assembly }, new Assembly[] { this.GetType().Assembly });

        try
        {
            Console.WriteLine("Executing command StartCommand");
            cm.ExecuteCommand(new StartCommand());
            Console.WriteLine("Command executed with success");
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception raised.\n{0}", ex.Message);
        }

        try
        {
            Console.WriteLine("Executing command null");
            cm.ExecuteCommand(null);
            Console.WriteLine("Command executed with success");
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception raised.\n{0}", ex.Message);
        }

        try
        {
            Console.WriteLine("Executing command OtherCommand");
            cm.ExecuteCommand(new OtherCommand());
            Console.WriteLine("Command executed with success");
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception raised.\n{0}", ex.Message);
        }

        try
        {
            Console.WriteLine("Trying to add commands already in the list of commands");
            cm.FillCommandsList(new Assembly[] { this.GetType().Assembly }, new Assembly[] { this.GetType().Assembly });
            Console.WriteLine("Command add with success");
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception raised.\n{0}", ex.Message);
        }
    }
}

最后,您不需要创建要存储在列表中的每个命令处理程序的实例。您也可以存储命令处理程序类型,并仅在必须执行时才以相同的方式实例化。

编辑: 我发现GetCustomAttribute仅在DotNet 4.5中可用。 如果您使用的是旧版本,则可以在此部分使用不同的代码:

object[] atts = commandType.GetCustomAttributes(typeof(CommandTypeAttribute), true);
if (atts.Count() <= 0)
    continue;

Type commandDataType = (atts[0] as CommandTypeAttribute).CommandType;

答案 2 :(得分:0)

编译器不允许您这样做,因为操作在概念上并不安全。您有一个方法要求其参数为StartCommand。它无法接受EndCommandSomeOtherCommand等。但是,通过将方法分配给Action<BaseCommand>,您具体说明它可以接受任何类型命令作为参数。如果此代码可以运行,您会发生什么?

public static void Start(StartCommand command) { }

Action<BaseCommand> action = Start;
action(new EndCommand());

现在,如果您碰巧能够确保这种情况不会发生,并且如果您将方法分配给Action<BaseCommand>,那么您将确保这种情况永远不会出现,那么您可以通过演员解决这个问题。通过将BaseCommand强制转换为委托正文中的StartCommand,该方法在技术上接受BaseCommand,它只会在运行时失败,除了{{1}之外的任何内容传递。

如果您完全控制了您正在处理的StartCommand个委托,并且能够确保始终传递正确的参数类型,那么您应该这样做。如果您将它们暴露给可能不了解这些约束的外部代码,那么这可能不是一个可行的解决方案。

在我上面的代码中,这个实现最简单地通过带有强制转换的lambda来完成:

Action<BaseCommand>

但是既然你正在使用反射,你可以使用一个调用你所拥有的Action<BaseCommand> action = command => Start((StartCommand)command); action(new EndCommand()); 的lambda,而不需要强制转换。

methodInfo