接口协变逆变:为什么不编译?

Interface covariance contravariance : why is this not compiling?

我想实现一个 CommandBus 可以 Dispatch 一些 CommandsCommandHandlers

我写的代码编译不通过。

编译器抱怨 cannot convert from 'IncrementHandler' to 'Handler<Command>'。 我不明白为什么,因为 IncrementHandler 实现 Handler<Increment>Increment 实现 Command

我已经在通用接口上尝试了 inout 修饰符,它没有解决问题。

有没有办法只用接口实现这个?

[TestClass]
public class CommandBusTest
{
  [TestMethod]
  public void DispatchesProperly()
  {
    var handler = new IncrementHandler(counter: 0);
    var bus = new CommandBus(handler); // <--Doesn't compile: cannot convert from 'IncrementHandler' to 'Handler<Command>'
    bus.Dispatch(new Increment(5));
    Assert.AreEqual(5, handler.Counter);
  }
}

public class CommandBus
{
  private readonly Dictionary<Type, Handler<Command>> handlers;

  public CommandBus(params Handler<Command>[] handlers)
  {
    this.handlers = handlers.ToDictionary(
      h => h.HandledCommand,
      h => h);
  }

  public void Dispatch(Command commande) { /*...*/ }
}

public interface Command { }

public interface Handler<TCommand> where TCommand : Command
{
  Type HandledCommand { get; }
  void Handle(TCommand command);
}

public class Increment : Command
{
  public Increment(int value) { Value = value; }

  public int Value { get; }
}

public class IncrementHandler : Handler<Increment>
{
  // Handler<Increment>
  public Type HandledCommand => typeof(Increment);
  public void Handle(Increment command)
  {
    Counter += command.Value;
  }
  // Handler<Increment>

  public int Counter { get; private set; }

  public IncrementHandler(int counter)
  {
    Counter = counter;
  }
}

这里的问题是 Increment 实现了 Command(我在下面的代码中将其重命名为 ICommand 以使其更清楚)。 因此它不再被接受为 Handler<Command>,这是构造函数所期望的(子类型而不是必需的超类型,正如@Lee 在评论中指出的那样)。

如果您可以归纳为只使用 ICommand,它会起作用:

public class CommandBusTest
{
    public void DispatchesProperly()
    {
        var handler = new IncrementHandler(counter: 0);
        var bus = new CommandBus((IHandler<ICommand>)handler); 
        bus.Dispatch(new Increment(5));
    }
}

public class CommandBus
{
    private readonly Dictionary<Type, IHandler<ICommand>> handlers;

    public CommandBus(params IHandler<ICommand>[] handlers)
    {
        this.handlers = handlers.ToDictionary(
          h => h.HandledCommand,
          h => h);
    }

    public void Dispatch(ICommand commande) { /*...*/ }
}

public interface ICommand { int Value { get; } }

public interface IHandler<TCommand> where TCommand : ICommand
{
    Type HandledCommand { get; }
    void Handle(TCommand command);
}

public class Increment : ICommand
{
    public Increment(int value) { Value = value; }

    public int Value { get; }
}

public class IncrementHandler : IHandler<ICommand>
{
    // Handler<ICommand>
    public Type HandledCommand => typeof(Increment);

    public void Handle(ICommand command)
    {
        Counter += command.Value;
    }

    // Handler<ICommand>

    public int Counter { get; private set; }

    public IncrementHandler(int counter)
    {
        Counter = counter;
    }
}

I don't understand why, because IncrementHandler implements Handler<Increment> and Increment implements Command

让我们消除你的误解,剩下的就清楚了。

假设你想做的是合法的。出了什么问题?

IncrementHandler ih = whatever;
Handler<Command> h = ih; // This is illegal. Suppose it is legal.

现在我们制作一个class

public class Decrement : Command { ... }

现在我们把它传递给 h:

Decrement d = new Decrement();
h.Handle(d);

这是合法的,因为Handler<Command>.Handle需要一个Command,而Decrement是一个Command

所以发生了什么事?您刚刚通过 h 将减量命令传递给 ih,但 ih 是一个只知道如何处理增量的 IncrementHandler

既然是无稽之谈,那么这里的某事肯定是不合法的;你希望哪条线是非法的? C# 团队决定转换应该是非法的。

更具体地说:

你的程序在尝试结束时使用反射-运行围绕类型系统的安全检查,然后你抱怨类型系统在你写一些不安全的东西时阻止你。您为什么 完全 使用泛型?

泛型(部分)是为了确保类型安全,然后你正在做一个基于反射的调度。这没有任何意义;不要采取措施来提高类型安全性,然后英勇地努力解决它们

显然您希望解决类型安全问题,因此根本不要使用泛型。只需制作一个 ICommand 接口和一个接受命令的 Handler class,然后有一些机制来计算如何调度命令。

我不明白的是为什么会有两种东西。如果要执行一个命令,那为什么不直接把执行逻辑放在命令对象上呢?

除了这种基于类型的笨拙字典查找之外,您还可以在此处使用其他设计模式。例如:

  • 命令处理程序可以有一个方法,该方法接受一个命令和 returns 一个布尔值,处理程序是否可以处理此命令。现在你有一个命令处理程序列表,一个命令进来了,你只需 运行 在列表中询问 "are you my handler?" 直到找到一个。如果 O(n) 查找速度太慢,则构建 MRU 缓存或记忆结果或类似的东西,摊销行为将会改善。

  • 调度逻辑可以放入命令处理程序本身。给命令处理程序一个命令;它要么执行它,要么递归调用其父命令处理程序。因此,您可以构建一个命令处理程序图,根据需要相互推迟工作。 (这基本上就是 QueryService 在 COM 中的工作方式。)

这里的问题是您对 Handler<TCommand> 的定义要求 TCommand 是协变的 逆变的 - 这是不允许的。

要将 Handler<Increment> 传递给 CommandBus 的构造函数(需要 Handler<Command>),您必须将 Command 声明为 [=19 中的协变类型参数=],像这样:

public interface Handler<out TCommand> where TCommand : Command

进行此更改后,您可以在请求 Handler<Command> 的任何地方传入 Handler<AnythingThatImplementsCommand>,因此 CommandBus 的构造函数现在可以工作了。

但这为以下行引入了一个新问题:

void Handle(TCommand command);

由于 TCommand 是协变的,因此可以将 Handler<Increment> 分配给 Handler<Command> 引用。然后您将能够调用 Handle 方法但传入 任何实现 Command 的东西 - 显然这是行不通的。要使 this 调用正确,您必须允许 TCommand 变为 contravariant

既然不能两全其美,那你就得在某个地方让步了。一种方法是在 Handler<TCommand> 中使用协方差,但在 Handle 方法中强制进行显式转换,如下所示:

public interface Handler<out TCommand> where TCommand : Command
{
    Type HandledCommand { get; }
    void Handle(Command command);
}

public class IncrementHandler : Handler<Increment>
{
    public void Handle(Command command)
    {
        Counter += ((Increment)command).Value;
    }
}

它不会阻止某人创建 IncrementHandler 然后传入错误类型的 Command,但是如果处理程序仅由 CommandBus 使用,您可以检查输入 CommandBus.Dispatch 并有类似类型安全的东西。