接口协变逆变:为什么不编译?
Interface covariance contravariance : why is this not compiling?
我想实现一个 CommandBus
可以 Dispatch
一些 Commands
到 CommandHandlers
。
- A
Command
是一个描述应该发生什么的简单 DTO。例如:"Increment counter by 5"
- A
CommandHandler
能够处理精确类型的 Command
。
CommandBus
接受一个Command
并执行能够处理它的CommandHandler
。
我写的代码编译不通过。
编译器抱怨 cannot convert from 'IncrementHandler' to 'Handler<Command>'
。
我不明白为什么,因为 IncrementHandler
实现 Handler<Increment>
而 Increment
实现 Command
我已经在通用接口上尝试了 in
和 out
修饰符,它没有解决问题。
有没有办法只用接口实现这个?
[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
并有类似类型安全的东西。
我想实现一个 CommandBus
可以 Dispatch
一些 Commands
到 CommandHandlers
。
- A
Command
是一个描述应该发生什么的简单 DTO。例如:"Increment counter by 5" - A
CommandHandler
能够处理精确类型的Command
。 CommandBus
接受一个Command
并执行能够处理它的CommandHandler
。
我写的代码编译不通过。
编译器抱怨 cannot convert from 'IncrementHandler' to 'Handler<Command>'
。
我不明白为什么,因为 IncrementHandler
实现 Handler<Increment>
而 Increment
实现 Command
我已经在通用接口上尝试了 in
和 out
修饰符,它没有解决问题。
有没有办法只用接口实现这个?
[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
implementsHandler<Increment>
andIncrement
implementsCommand
让我们消除你的误解,剩下的就清楚了。
假设你想做的是合法的。出了什么问题?
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
并有类似类型安全的东西。