"supposedly" 正确类型的 Kotlin 类型推断

Kotlin type inference on "supposedly" right types

我是 Kotlin 的新手,我正在玩它。我非常想创建一个非常基本的事件总线。所以我想到了这个

interface Event
interface EventListener<E : Event> {
    fun handle(event: E)
}

interface EventBus {
    fun <E : Event> registerListener(aClass: Class<E>, eventListener: EventListener<E>)
}

class MyBus() : EventBus {
    private val eventListeners: MutableMap<String, MutableList<EventListener<out Event>>> = mutableMapOf()

    constructor(listeners: List<Pair<Class<Event>, EventListener<Event>>>) : this() {
        listeners.forEach {
            registerListener(it.first, it.second)
        }
    }

    override fun <E : Event> registerListener(aClass: Class<E>, eventListener: EventListener<E>) {
        val key = aClass.name
        val listeners: MutableList<EventListener<out Event>> = eventListeners.getOrPut(key) { mutableListOf() }
        listeners.add(eventListener)
    }
}


val bus = MyBus(
    listOf(
        MyEvent::class.java to MyEventListener()
    )
)

class MyEvent : Event
class AnotherEvent : Event
class MyEventListener : EventListener<MyEvent> {
    override fun handle(event: MyEvent) {
    }
}

发生的事情是,当我尝试使用接受对列表的构造函数创建 MyBus 时,我得到

Type inference failed. Expected type mismatch: inferred type is List<Pair<Class<MyEvent>,MyEventListener>> but List<Pair<Class<Event>,EventListener<Event>>> was expected

但是如果我将构造函数更改为类似

constructor(listeners: List<Pair<Class<out Event>, EventListener<out Event>>>) : this() {
        listeners.forEach {
            registerListener(it.first, it.second)
        }
    }

几乎所有地方都相加,然后 MyBus 构造函数工作,但对 registerListener(..) 的调用因与以前相同的确切原因而中断。所以解决这个问题的唯一方法是在 registerListener 函数上也添加“out”。

我怀疑我在这里做错了什么,但我不知道具体是什么。有帮助吗?

如果您希望 EventListener 能够使用 Events,那么它的类型必须是 invariantcovariant (未声明 [​​=18=])。如果它让你传递你的 EventListener<MyEvent> 就好像它是一个 EventListener<Event>,那么你的 MyBus class 可能会用一些 Event 调用 listener.handle(event)那不是 MyEvent,例如 AnotherEvent。然后你会得到一个 ClassCastException 当它试图将这个 AnotherEvent 转换为 MyEvent.

为了能够存储不同类型的不变EventHandlers,您将不得不使用星投影去除方差限制,并在从地图中检索它们时投射它们。因此,将映射键变成 class 对象而不仅仅是字符串。由于在使用星形投影类型时您不会得到编译器的帮助,因此您需要小心,您只是将一个项目添加到您的 MutableMap 中,该项目与关联的 Class 键具有相同的类型用它。然后当你检索项目时,只转换为不变类型。

您问题的另一部分是您的构造函数需要泛型。现在它只与 Event 一起工作,所以它不能处理 Event 的子类型。 Kotlin 不(还?)支持构造函数的泛型类型,因此您必须使用工厂函数来实现。

这是上述所有内容的示例。

class MyBus() : EventBus {
    private val eventListeners: MutableMap<Class<*>, MutableList<EventListener<*>>> = mutableMapOf()

    override fun <E : Event> registerListener(aClass: Class<E>, eventListener: EventListener<E>) {
        val listeners = retrieveListeners(aClass)
        listeners.add(eventListener)
    }

    private fun <E: Event> retrieveListeners(aClass: Class<E>): MutableList<EventListener<E>> {
        @Suppress("UNCHECKED_CAST")
        return eventListeners.getOrPut(aClass) { mutableListOf() } as MutableList<EventListener<E>>
    }
}

// Factory function
fun <E : Event> myBusOf(listeners: List<Pair<Class<E>, EventListener<E>>>): MyBus {
    return MyBus().apply {
        listeners.forEach {
            registerListener(it.first, it.second)
        }
    }
}

并且您可能希望将工厂参数的类型从 <List>Pair 更改为 vararg Pair,以便更易于使用。


这是一个解释方差限制的精简示例。

您的事件消费者界面:

interface EventListener<E : Event> {
    fun handle(event: E)
}

Event的两个实现:

class HelloEvent: Event {
   fun sayHello() = println("Hello world")
}

class BoringEvent: Event {}

A class 实现接口:

class HelloEventListener: EventListener<HelloEvent> {
    override fun handle(event: HelloEvent) {
        event.sayHello()
    }
}

现在您有一个可以处理 HelloEvent 的 EventListener。尝试将其视为 EventListener<Event>:

val eventListener: EventListener<Event> = HelloEventListener() // COMPILE ERROR!

假设编译器没有阻止你这样做而你这样做:

val eventListener: EventListener<Event> = HelloEventListener()
eventListener.handle(BoringEvent()) // CLASS CAST EXCEPTION AT RUN TIME!

如果允许这样做,您的 HelloEventListener 将尝试在没有该功能的 BoringEvent 上调用 sayHello(),因此它会崩溃。这就是泛型在这里保护您免受的影响。

现在假设您的 HelloEventListener.handle() 没有调用 event.sayHello()。那么,它就可以安全地处理 BoringEvent。但是编译器并没有为您进行那种级别的分析。它只知道您声明的内容,HelloEventListener 无法处理除 HelloEvent 之外的任何内容。