【问题标题】:Map of generic interfaces in KotlinKotlin 中的通用接口图
【发布时间】:2021-07-15 00:11:17
【问题描述】:

我坚持一些简单的事情)假设我有以下内容:

interface IMessagePayload // marker interface

data class IdPayload(
    val id: Long
) : IMessagePayload

data class StringPayload(
    val id: String,
) : IMessagePayload

那我有课了:

data class Message<T : IMessagePayload>(
    val id: String,
    val payload: T,
)

我还有一些描述此消息处理器的接口:

interface IMessageProcessor<T : IMessagePayload> {
    fun process(message: Message<T>)
}

还有一些实现:

class ProcessorImpl : IMessageProcessor<IdPayload> {
    override fun process(message: Message<IdPayload>) {
    }
}

现在我想要一张此类处理器的地图。让我们使用一些枚举类型作为这个映射的键:

enum class ActionType {
     UPDATE,
     DELETE,
     ADD
}
private var map = mutableMapOf<ActionType, IMessageProcessor<IMessagePayload>>()

map[ActionType.ADD] = ProcessorImpl() // <-- error here

这就是问题所在。我无法将 ProcessorImpl 放入此地图。编译器说有一个错误:类型不匹配。必需:IMessageProcessor。找到:ProcessorImpl()

我可以通过以下方式声明地图(使用星形投影):

private var map = mutableMapOf<ActionType, IMessageProcessor<*>>()

但在这种情况下,我不能调用处理器的 process 方法,首先按键从地图中获取它:

map[ActionType.ADD]?.process(Message("message-id", IdPayload(1))) // <-- error here

编译器抱怨:类型不匹配。什么都不需要。找到Message&lt;IdPayload&gt;

我做错了什么?任何帮助表示赞赏。

【问题讨论】:

    标签: kotlin generics


    【解决方案1】:

    这是关于variance

    IMessageProcessor 定义为interface IMessageProcessor&lt;T : IMessagePayload&gt;;它有一个类型参数,必须是IMessagePayload 或子类型。

    但它在那个类型参数中是不变的; IMessageProcessor&lt; IdPayload&gt;IMessageProcessor&lt;IMessagePayload&gt; 无关。特别是,它不是一个子类型。

    您的map 是用值类型IMessageProcessor&lt;IMessagePayload&gt; 定义的。所以它的值不能是IMessageProcessor&lt; IdPayload&gt;,因为那既不是值类型,也不是子类型。因此编译错误。


    在这种情况下,让它编译的最简单方法是更改​​您的map

    private var map = mutableMapOf<ActionType, IMessageProcessor<out IMessagePayload>>()
    

    唯一的区别是out;这告诉编译器值 IMessageProcessor 在其类型参数中是协变的。 (将out 视为“……或任何子类型”可能会有所帮助。  类似地,您可以使用in 使其成为逆变,您可能会将其视为“……或任何超类型”。)

    这使您可以在映射中为IMessagePayload 的任何子类型存储IMessageProcessor

    但是,如果您这样做,您会发现您无法使用从地图中提取的任何值——因为它无法判断处理器可以处理哪些消息,即它适用于IMessagePayload 的哪个子类型! (编译器将此表示为期望Nothing 的类型参数。)

    通常,最好在接口或超类本身(declaration-site variance)上指定方差,而不是上面显示的use-site variance。但是我在这里看不到一个好的方法,因为你有多个泛型类,它们以复杂的方式交互......)


    想一想IMessageProcessor 的类型参数是什么意思:它是处理器可以使用的消息类型。所以IMessageProcessor&lt;A&gt; 可以处理Message&lt;A&gt; 类型的消息。

    现在,子类型必须能够完成它的超类型可以做的所有事情(通常更多)——否则你不能在任何期望使用超类型的地方删除那个子类型。 (这有 Liskov substitution principle 的盛名——但这实际上只是常识。)

    所以IMessageProcessor&lt;B&gt;IMessageProcessor&lt;A&gt; 的子类型,前提是它至少可以处理IMessageProcessor&lt;A&gt; 可以处理的所有消息。这意味着它必须接受所有Message&lt;A&gt; 类型的消息。

    但是Message 的类型参数是不变的:Message&lt;B&gt;Message&lt;A&gt; 没有直接关系。所以你不能编写一个同时处理它们的处理器。

    我能找到的最自然的解决方案是在 MessageIMessageProcessor 上指定方差:

    data class Message<out T : IMessagePayload>( /*…*/ )
    
    interface IMessageProcessor<in T : IMessagePayload> { /*…*/ }
    

    然后在你的地图中使用通配符来明确表明你对它的值的类型参数一无所知:

    private var map = mutableMapOf<ActionType, IMessageProcessor<*>>()
    

    这样您就可以在地图中安全地存储 ProcessorImpl()

    但您仍然必须对从地图中拉出的值使用(未经检查的)强制转换,然后才能使用它们:

    (map[ActionType.ADD] as IMessageProcessor<IdPayload>)
    .process(Message("4", IdPayload(4L)))
    

    我认为没有任何简单的方法可以解决这个问题,因为问题在于具有只能处理某些(未知)类型消息的处理器的值。

    恐怕最好的办法是重新考虑这些类的含义以及它们应该如何交互,并相应地重新设计。

    【讨论】:

    • 非常感谢您的清晰解释!你能给我一些建议如何处理这个问题吗?你会如何重新设计这个系统?
    猜你喜欢
    • 2020-10-07
    • 1970-01-01
    • 2018-11-28
    • 2022-01-05
    • 1970-01-01
    • 2021-10-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多