以允许可空类型和使用该类型参数声明“KClass<T>”的方式声明函数泛型类型参数

Declare a function generic type parameter in a way that allows both for nullable types and using that type parameter to declare `KClass<T>`

KClass 定义为 public interface KClass<T : Any> : KDeclarationContainer, KAnnotatedElement, KClassifier

这很棘手,因为 String? 的 class 应该是 KClass<String>,但无法获得。

给定下面的 3 个示例(基本上都应该完成相同的工作),其中 1 个无法编译,其他 return 相同的运行时类型。

inline fun <reified T> test1(): Any = T::class
inline fun <reified T: Any> test2(): KClass<T> = T::class
inline fun <reified T> test3(): KClass<T> = T::class // does not compile

test1<String?>() // class kotlin.String
test1<String>() // class kotlin.String
test2<String?>() // does not compile
test2<String>() // class kotlin.String

问题的重点是要问:如何获得test1的运行时行为和test2的编译时行为(和安全性)?

编辑: 该问题的最后一个附录是另一个示例,它演示了获取可空类型的 class 的问题。

inline fun <reified T> test4() {
    val x = T::class // compiles, implied type of x is KClass<T>
    val y: KClass<T> = T::class // does not compile with explicit type of KClass<T>
}

我特别遇到问题的调用站点是:

class OutputContract<T>(
    private val output: () -> T,
    val outputType: KClass<T>   // ERROR!
) {    
    fun invoke(): T {
        return output()
    }
}


inline fun <reified T> output(noinline output: () -> T): OutputContract<T> {
    return OutputContract(output, T::class)
}

这里唯一的错误是 KClass<T>,而不是 T::class,它嗡嗡作响。我想允许消费者将可空性指定为合同的一部分,因此添加 Any 约束将不起作用。如果我只是将 KClass<T> 变成 KClass<Any>,这一切都有效(这证明没有运行时问题,只有编译时间)。这最终是我选择的解决方法,但如果我能真正保持正确的类型,那就太好了。

注意:这是在没有提供足够信息以了解真正目标是什么的情况下陈述问题的答案。暂时留在这里以防其他人来这里寻找 "other" 东西。

可空性是 KType 的一部分,而不是 KClass 的一部分,因此您想从具体化参数创建一个 KType。这样做并不容易,这个概念已经在 KT-15992 中讨论过了。亚历山大·乌达洛夫 (Alexander Udalov) 撰写的那个问题中提到了一些原型代码,但它可能已过时并且绝对不包括可空性。

如果您将具体化参数转换为 KClass,您将始终丢失可空性信息,因此围绕 KClass 的任何解决方案本身都无法满足您的需求。

似乎有一种解决方法可以单独获取可空性信息,您可以使用 T::class.createType(...) 重建您自己的 KType 但这有点复杂并且尚未在所有情况下得到证实. Ruslan Ibragimov 提出的从具体化的 T 中查找可空性的解决方法如下:

inline fun <reified T : Any?> sample() {
    println("Is nullable: ${isNullable<T>()}")
}

inline fun <reified T : Any?> isNullable(): Boolean {
    return null is T
}

fun main(args: Array<String>) {
    sample<String?>()
    sample<String>()
}

// Is nullable: true
// Is nullable: false

无论采用哪种方法,您仍然需要以 KType 结束。


实验性完整解决方案:

我根据 Alexander Udalov 和 Ruslan Ibragimov 的示例代码创建了一个推断完整 KType 的实验性实现。您可以在 Klutter library source code 中看到此代码,它在 Klutter 版本 2.5.3 中发布,其中标有实验性警告。

您可以查看 test code 以了解它是如何工作的,但基本上很简单:

val nullableString = reifiedKType<String?>()
println(nullableString.isMarkedNullable) // true

val nonNullableString = reifiedKType<String>()
println(nonNullableString.isMarkedNullable) // false

您的问题缺少最重要的信息。您展示了一个将函数调用为 myFun<String?>() 的人为案例,但如果是这种情况,您显然可以将其更改为不使用可空类型。所以这可能不是真正的用例。您过于简化了您的解释并删除了我们回答您的问题所需的最相关信息:“什么是完整的方法签名以及调用站点是什么样的?

缺少的是你如何推断​​你的类型T?您可以从 return 值、方法参数或通过在每个调用站点中显式声明它来获取它。因此,您可以选择在您的函数中使用 T: Any,并根据您在问题中未显示的信息来决定哪个是最好的。

所以这是你的选择:

  1. 如果您根据 return 参数推断类型,则允许 return 可为空但不要使具体化类型可为空:

    // call site, any return type nullable or not
    val something: String? = doSomething()
    
    // function
    inline fun <reified T: Any> doSomething(): T? {
        val x: KClass<T> = T::class
        // ...
    }
    
  2. 或者,如果您是从传入参数推断它,请在那里执行相同的技巧:

    // call site, any parameter type nullable or not
    val param: String? = "howdy"
    doSomethingElse(param)
    
    // function
    inline fun <reified T: Any> doSomethingElse(parm: T?) {
        val x: KClass<T> = T::class
        // ...
    }
    
  3. 或者您实际上是在指定通用参数(只是在键入参数名称时不要使其可为空):

    // call site, any non-nullable generic parameter
    doSomething<String>()
    
    // function
    inline fun <reified T: Any> doSomethingElse() {
        val x: KClass<T> = T::class
        // ...
    }
    
  4. 如果你不能改变通用参数(但你为什么不能?!?):

    // call site: whatever you want it to be
    
    // function:
    inline fun <reified T> test4() {
        val x = T::class // compiles, implied type of x is KClass<T>
        val y: KClass<*> = T::class  KClass<T>
    }
    

    顺便说一下,xy 的作用是一样的,KClass 引用中会缺少一些 methods/properties。

这些示例中有四分之三可以满足您的需求,我无法想象其中一个不起作用的情况。否则,您如何推断 T 类型?上面的不适用的模式是什么?

注意在同一方法签名中使用 <T: Any>T? 的技巧。

根据您对问题的最新更新,这保持了您对 output 函数引用的可空性,但允许它适用于 KClass:

class OutputContract<T: Any>(private val output: () -> T?, val outputType: KClass<T>) {
    fun invoke(): T? {
        return output()
    }
}

inline fun <reified T: Any> output(noinline output: () -> T?): OutputContract<T> {
    return OutputContract(output, T::class)
}

用户仍然可以通过传入不 return 空值的输出实现来控制可空性,Kotlin 仍将对它们进行类型检查并正常运行。但是必须检查对 invoke 的调用,因为它始终被假定为可为空。你真的不能同时拥有它,想要对 T 进行可空性控制但在内部将其用作类型化 KClass 但你 可以 将其用作 KClass<*> 取决于您使用 KClass 中的什么功能。你可能不会错过任何重要的事情。你没有显示你打算用 KClass 做什么,所以很难在这个话题上多说。 KClass 通常不是一个好的类型,如果你认为他们可能会给你一个通用的 class,你应该改用 KType

图片来自Understanding Generics and Variance in Kotlin