我会很谨慎。但只要区分内联类与其字段并不重要,您可能就可以了。
Retrofit 的工作原理是在运行时根据传递给Retrofit::create 的接口的方法注释构造一个代理类。当第一次调用接口方法时,代理使用反射来检查方法的签名和注释,然后用于构造实现。我们感兴趣的是,当类型签名提到内联类时,反射会看到什么。
“安全”问题(这里提到的那种)实际上是接口契约的问题:哪些行为可以保证是稳定的,哪些可能会根据其版本、与其他功能的交互、发现的实现随心所欲地改变优化机会或月相。在这种特殊情况下,我们想知道 Kotlin 内联类的 JVM ABI。
人们可能希望 Kotlin 规范可以详细阐述该主题。但可惜的是,唯一的 Kotlin 规范只定义了语言的抽象约束,没有任何 ABI 细节。以下是the specification (version 1.5-rfc+0.1) 关于反射的全部内容(第 16.2 节):
特定平台可以通过 reflection 的方式为运行时类型自省提供更复杂的工具 - 标准库的特殊平台提供的部分,允许在运行时访问有关类型和声明的更详细信息。但是,它是特定于平台的,必须参考特定平台文档以了解详细信息。
这就是关于内联类的表示的内容(第 4.1.5 节):
[A]n 值 [sic] 类被实现允许在适用的情况下内联,以便对其数据属性进行操作。这也意味着,如果编译器认为这样做是正确的,则可以随时使用其主构造函数将属性装箱回值类。 [强调我的]
但是在实践中应该期待什么? ABI 很少被故意破坏,因为这往往具有很大的破坏性,所以我们可以指望它不会改变太多。手册虽然没有具体说明,但在the section on representation of inline classes 中仍然包含一些非常有指导意义的示例:
由于内联类被编译为它们的底层类型,它可能会导致各种难以理解的错误,例如意外的平台签名冲突:
@JvmInline
value class UInt(val x: Int)
// Represented as 'public final void compute(int x)' on the JVM
fun compute(x: Int) { }
// Also represented as 'public final void compute(int x)' on the JVM!
fun compute(x: UInt) { }
为了缓解此类问题,通过在函数名称中添加一些稳定的哈希码,对使用内联类的函数进行了修改。所以fun compute(x: UInt)会被表示为public final void compute-<hashcode>(int x),这样就解决了冲突问题。
因此,我们应该期望一个采用内联类参数的方法具有一个类型签名,其中底层字段被替换,并且名称混乱。这也应该适用于接口方法。但我没有看到任何线索表明 Retrofit 可以解释这些,即使它尝试过也无法解释:无法反转重整以发现实际的底层类型。对于库来说,该方法看起来就像采用底层类型的任何其他方法,只是名称很奇怪。这意味着您可能会遇到以下问题:
@JvmInline
value class MyId(val raw: String) {
override fun toString(): String = "where is your god now?"
}
interface MyApiEndpoints {
@GET("/fetch/{id}")
suspend fun fetch(@Path("id") id: MyId): Response<JsonObject>
}
正如@Path 的文档所解释的那样:
使用Retrofit.stringConverter(Type, Annotation[])(或Object.toString(),如果未安装匹配的字符串转换器)将值转换为字符串,然后进行 URL 编码。
在 JVM 级别,fetch 将有一个带有String 参数的类型签名,Retrofit 生成的实现将这样对待它。因此,它不会调用toString 的自定义实现。 (在 Kotlin 中,这可以正常工作,因为 Kotlin 在编译时知道类型并静态分派方法。)
但是在内联类和它的包装字段之间的区别并不重要的情况下,你只是可能能够摆脱它。