声明一个函数泛型类型参数,允许两者都为可空类型,并使用该类型参数声明`KClass <t>

时间:2018-04-02 23:31:47

标签: kotlin kotlin-reflect

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

这很棘手,因为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

问题的关键是要问:如何使用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>
}

我特别遇到问题的呼叫网站是:

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>,这一切都有效(这证明没有运行时问题,只有编译时间)。这最终是我选择的解决方法,但如果我能够保持正确的类型,那就太好了。

2 个答案:

答案 0 :(得分:2)

注意: 如果没有说明问题并且有足够的信息来了解真正的目标是什么,这就是答案。现在离开这里以防其他人来这里寻找&#34;其他&#34;的事情。

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

如果您将reified参数转换为KClass,那么您将始终失去可为空性信息,因此KClass周围的解决方案本身无法获得您想要的效果。

似乎有一种解决方法可以单独获取可为空性信息,您可以使用KType重建自己的T::class.createType(...),但这有点复杂,并且在所有情况下都未得到证实。 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

答案 1 :(得分:2)

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

缺少的是你如何提出你的类型T?您可以从返回值,方法参数中获取它,也可以在每个调用站点中明确说明它。因此,您可以选择在您的函数中使用T: Any,并确定哪个最佳取决于您未在问题中显示的信息。

所以这是您的选择:

  1. 如果基于return参数推断类型,则允许返回可为空,但不要使reified类型为空:

    // 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引用中缺少某些方法/属性。

  5. 这些例子中有四分之三会让你想要你,而我无法想象其中一个例子不起作用的情况。否则,您如何推断类型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)
    }
    

    用户仍然可以通过传递不返回空值的输出实现来控制可空性,Kotlin仍会键入检查它们并且正常运行。但是必须检查调用调用,因为它总是被认为是可空的。你无法真正拥有这两种方式,希望对T进行可控性控制,但在内部将其用作已键入的KClass,但可以将其用作KClass<*>取决于您在KClass中使用的功能。你可能没有遗漏任何重要的东西。您没有显示您打算使用KClass进行的操作,因此很难对该主题进行更多说明。 KClass通常不是一个好的类型,如果您认为它们可能会通过一个通用类,您应该使用KType代替。

    Image from Understanding Generics and Variance in Kotlin 来自Understanding Generics and Variance in Kotlin

    的图片