考虑以下通过使用类型标记“具体化”的泛型类:
class Gen<X : Any>(val token: KClass<X>) {
fun doSomething(x: X) {
// ...
}
}
现在
Gen
使用星形投影进行异构使用,但类型标记可用于匹配拟合值:
fun m(g: Gen<*>, x: Any) {
if (g.token == x::class) {
g.doSomething(x)
}
}
这无法编译。引入以下未经检查的强制转换可以使代码编译,但不安全,因为强制转换值可能会提供错误的输入(当前代码实际上不会抛出 ClassCastException,但可以轻松扩展以执行此操作)。
fun m(g: Gen<*>, x: Any) {
if (g.token == x::class) {
g as Gen<Any>
g.doSomething(x)
g.doSomething(8) // potential ClassCastException
}
}
这是处理此问题的唯一方法还是有更多类型安全的方法来做到这一点?
根据经验,理解
Gen
内部逻辑并执行未经检查的转换的代码应该“接近”它。它可以是同一组件提供的成员函数或扩展函数。根据具体情况,有一些方法可以使其更易于使用或更好地保证类型安全。
如果我们需要为
doSomething
的用户提供类型安全的Gen
功能,我们可以轻松实现:
fun Gen<*>.doSomethingIfMatches(x: Any) {
if (token == x::class) {
@Suppress("UNCHECKED_CAST")
(this as Gen<Any>).doSomething(x)
}
}
如果我们实现“关闭”函数,那么我们通常可以进行未经检查的强制转换,但我们希望将无类型代码限制为仅函数的开头,然后使用类型安全代码,我们可以使该函数泛型并使用
T
作为“动态类型”:
fun <T : Any> m(g: Gen<*>, x: T) {
if (g.token == x::class) {
@Suppress("UNCHECKED_CAST")
g as Gen<T>
g.doSomething(x)
g.doSomething(8) // compile error
}
}
如果我们有多个函数消耗或返回
X
并且我们需要一个实用程序从非类型化代码环境切换到类型化代码环境,我们可以这样做:
fun main() {
val g: Gen<*> = Gen(String::class)
g.asTypedOrNull<String>()?.doSomething("hello") // executes
g.asTypedOrNull<Int>()?.doSomething(42) // doesn't execute
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Any> Gen<*>.asTypedOrNull(): Gen<T>? = (this as Gen<T>).takeIf { token == T::class }
请注意,与您的代码和上面的示例相反,它使用编译时类型,而不是运行时类型。如果我们需要将逻辑基于运行时类型,那么这会更棘手,但仍然是可能的:
fun main() {
val g: Gen<*> = Gen(String::class)
g.ifMatches("hello") { doSomething(it) } // executes
g.ifMatches(42) { doSomething(it) } // doesn't execute
}
inline fun <T : Any> Gen<*>.ifMatches(x: T, block: Gen<T>.(T) -> Unit): Unit {
if (token == x::class) {
@Suppress("UNCHECKED_CAST")
(this as Gen<T>).block(x)
}
}
在 lambda 内部,我们保证接收者和
it
匹配,并且我们可以一起使用它们。