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

Bur*_*urg 5 kotlin kotlin-reflect

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

这很棘手,因为 a 的类String?应该是KClass<String>,但不可能获得。

给出下面的 3 个示例(它们本质上应该执行相同的工作),其中 1 个无法编译,其他返回相同的运行时类型。

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
Run Code Online (Sandbox Code Playgroud)

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

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

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>
}
Run Code Online (Sandbox Code Playgroud)

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

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)
}
Run Code Online (Sandbox Code Playgroud)

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

Jay*_*ard 3

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

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

所以这是你的选择:

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

    // 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
        // ...
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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
        // ...
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 或者您从字面上指定通用参数(只是在输入参数名称时不要使其可为空):

    // call site, any non-nullable generic parameter
    doSomething<String>()
    
    // function
    inline fun <reified T: Any> doSomethingElse() {
        val x: KClass<T> = T::class
        // ...
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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>
    }
    
    Run Code Online (Sandbox Code Playgroud)

    顺便说一句,x和 的y行为相同,并且参考中将缺少一些方法/属性KClass

这些例子中有四分之三可以满足你的愿望,我无法想象其中一个不起作用的情况。否则,你如何推断​​类型T?与上述不兼容的模式是什么?

注意在同一方法签名中使用<T: Any>with的技巧。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)
}
Run Code Online (Sandbox Code Playgroud)

用户仍然可以通过传入不返回 null 的输出实现来控制可为 null 性,Kotlin 仍会对其进行类型检查并正常运行。但是必须检查对 invoke 的调用,因为它始终被假定为可为空。您实际上不能同时拥有这两种方式,想要启用可空性控制T,但在内部将其用作类型KClass,但您可以将其KClass<*>用作取决于您在KClass. 您可能不会错过任何重要的事情。您没有表明您打算如何处理,KClass因此很难就该主题说更多内容。 KClass如果您认为它们可能会向您传递泛型类,则通常不是一个好的类型,您应该使用 aKType代替。

图片来自理解 Kotlin 中的泛型和变体 图片来自理解 Kotlin 中的泛型和变体