【问题标题】:Avoid unchecked cast避免未经检查的演员表
【发布时间】:2020-04-26 01:13:44
【问题描述】:

在以下情况下,我找不到避免未经检查的强制转换的方法

class EventBus {
    val eventToHandle: MutableMap<KClass<out Event>, Event.() -> Unit> = mutableMapOf()

    final inline fun <reified T : Event> register(noinline handler: T.() -> Unit) {
        @Suppress("UNCHECKED_CAST")
        eventToHandle[T::class] = handler as Event.() -> Unit
    }

    fun fire(event: Event) {
        eventToHandle[event::class]?.invoke(event)
            ?: throw IllegalStateException("Missing handler for class ${event::class}")
    }
}

我的目标是强制映射仅包含具有 Event(或其子类型之一)作为接收者的 lambda,但同时我想要参考 actual 的实现Event 注册 lambda 时。 通过这种方式,我可以使用他的实现成员,而无需每次都进行强制转换。

一个例子(requestIdRequestExpiredEvent的一个字段):

eventBus.register<RequestExpiredEvent> {
            requestService.setExpiredByRequestId(requestId)
        }

我知道“消费者 lambda”没有协方差并且它们是逆变的,但我想是否有办法。 我发现一个丑陋的解决方法是:

    final inline fun <reified T : Event> register(crossinline block: T.() -> Unit) {
        val handler: Event.() -> Unit = { this as T; block(this) }
        eventToHandle[T::class] = handler
    }

谢谢

【问题讨论】:

  • 你可以简化第二个版本eventToHandle[T::class] = { block(this as T) },但这仍然会导致ClassCastException,实际上它与前一个没有区别

标签: generics kotlin casting


【解决方案1】:

这是一个危险的演员阵容。 SomeEventSubtype.() -&gt; Unit 不是 Event.() -&gt; Unit 的子类型。反之亦然。

假设 Event 是一个开放类,并且你有这个子类:

class SubEvent: Event() {
    fun hello(): Unit {
        println("hello")
    }
}

现在你尝试投射它:

val hello: SubEvent.() -> Unit = SubEvent::hello
val helloCasted = hello as Event.() -> Unit

当您尝试调用helloCasted.invoke(Event()) 时,它会在尝试将您的事件转换为子事件时抛出 ClassCastException。您不能使用 Event 的任何实例作为输入来调用 hello,因为只有 SubEvent 具有要调用的 hello 函数。

如果您尝试隐式转换,编译器会捕获此错误:

val hello: SubEvent.() -> Unit = SubEvent::hello
val helloCasted: Event.() -> Unit = hello // compiler error

反之亦然。查看函数的输入时,您可以将类型层次结构视为倒置的。

val toStringFun: Event.() -> Unit = Event::toString
val toStringCasted: SubEvent.() -> Unit = toStringFun // OK

如果不在某处进行未经检查的强制转换,则无法解决此问题,因为您在地图中存储了不同类型的对象。但是您需要将您的投射移动到 fire 函数,以便它知道将它投射到什么。您可以将函数存储为类型 Any 在地图中,因为无论如何您都会投射它们。像这样的:

class EventBus {
    val eventToHandle: MutableMap<KClass<out Event>, Any> = mutableMapOf()

    inline fun <reified T : Event> register(noinline handler: T.() -> Unit) {
        eventToHandle[T::class] = handler
    }

    @Suppress("UNCHECKED_CAST")
    inline fun <reified T : Event> fire(event: T) {
        (eventToHandle[T::class] as? T.() -> Unit)?.invoke(event)
            ?: throw IllegalStateException("Missing handler for class ${event::class}")
    }
}

【讨论】:

  • 我同意您的解决方案。将@Suppress("UNCHECKED_CAST")放在fire方法中是一个更好的设计。顺便说一句,我完全知道关于 lambda 的协变和逆变是什么意思,但是我找不到我以前的代码可以解决 ClassCastExcepion 的方法。注册一个没有与正确的KClass 耦合的 lambda 是不可能的。我错过了什么吗?谢谢顺便说一句!
  • 哦,也许你的做法没问题。在查看您如何实际检索和调用这些函数之前,我编写了第一部分。由于类型擦除,我想它永远不必强制转换事件,因为你让它们正确匹配到它们的函数类型。但我认为将地图的关键类型设为您知道的事实对于您放入的实际对象不正确是没有意义的。它应该是Nothing.() -&gt; Unit 或更简单的Any,因为每次检索它们时,您都在投射它。
  • 也是如此。我能问你为什么将函数声明为Nothing.() -&gt; Unit 有意义吗?我知道每个T.() -&gt; Unit 都是一个子类型,因此不需要强制转换,但逻辑上我发现它具有误导性。我什至无法理解它应该是Nothing.() -&gt; Unit 类型的合法函数:D
  • “但我认为将地图的键类型设置为你知道的事实对于你放入的实际对象是不正确的”这个想法是明确的地图的关键不是Any 对象,而是事件的函数
  • 由于Nothing 是所有事物的子类型,Nothing.() -&gt; Unit 是具有单个输入并返回Unit 的所有函数的超类型。所以这让你更接近于表达地图中的内容。不幸的是,没有办法在函数类型声明的输入类型上设置上限,因此您无法将其绑定在 EventNothing 之间。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-20
  • 2017-04-22
  • 1970-01-01
相关资源
最近更新 更多