将 Kotlin 内联类传递给 Retrofit 函数是否安全

Eli*_*zer 4 kotlin retrofit retrofit2

如果我有以下内联类:

interface StringId {
  val raw: String
}

@JvmInline
value class MyId(override val raw: String) : StringId
Run Code Online (Sandbox Code Playgroud)

将其传递给 Retrofit 函数是否安全?

interface MyApiEndpoints {
  @GET("/fetch/{id}")
  suspend fun fetch(@Path("id") id: MyId): Response<JsonObject>
}
Run Code Online (Sandbox Code Playgroud)

use*_*170 10

我会很谨慎。但只要区分内联类与其字段并不重要,就可能问题。

\n

Retrofit 的工作原理是在运行时根据传递给 的接口的方法注释构造代理类Retrofit::create。当首次调用接口方法时,代理使用反射来检查方法\xe2\x80\x99s 签名和注释,然后使用它们来构造实现。我们感兴趣的是当类型签名提到内联类时反射会看到什么。

\n

\xe2\x80\x98safety\xe2\x80\x99 的问题(这里提出的类型)实际上是接口契约的问题:哪些行为保证是稳定的,哪些行为可以在实现上改变\xe2\x80\x99s突发奇想取决于它的版本、与其他功能的交互、发现的优化机会或月相。在这种特殊情况下,我们想了解 Kotlin 内联类的 JVM ABI。

\n

人们可能希望 Kotlin 规范能够详细阐述该主题。但可惜的是,唯一的 Kotlin 规范只定义了该语言的抽象约束,没有任何 ABI 细节。以下是规范(版本 1.5-rfc+0.1)关于反射(\xc2\xa716.2)的所有内容:

\n
\n

特定平台可以通过反射\xe2\x80\x94 标准库的特殊平台提供的部分的方式为运行时类型自省提供更复杂的设施,该部分允许在运行时访问有关类型和声明的更详细信息。然而,它是特定于平台的,必须参阅特定平台文档以了解详细信息。

\n
\n

这就是关于内联类的表示(\xc2\xa74.1.5)的内容:

\n
\n

[A]n value [sic] 类在适用的情况下允许内联以便对其数据属性进行操作。这也意味着,如果编译器认为这样做是正确的,则可以随时使用其主构造函数将该属性装回值类。[强调我的]

\n
\n

但在实践中应该期待什么?ABI 很少被故意破坏,因为这往往具有很大的破坏性,所以我们可以指望它不会改变太多。该手册虽然没有规范,但在有关内联类表示的部分中仍然包含一些非常有指导意义的示例:

\n
\n

由于内联类被编译为其基础类型,因此可能会导致各种模糊错误,例如意外的平台签名冲突:

\n
@JvmInline\nvalue class UInt(val x: Int)\n\n// Represented as \'public final void compute(int x)\' on the JVM\nfun compute(x: Int) { }\n\n// Also represented as \'public final void compute(int x)\' on the JVM!\nfun compute(x: UInt) { }\n
Run Code Online (Sandbox Code Playgroud)\n

为了缓解此类问题,通过向函数名称添加一些稳定的哈希码来破坏使用内联类的函数。因此,fun compute(x: UInt)将表示为public final void compute-<hashcode>(int x),这解决了冲突问题。

\n
\n

因此,我们应该期望采用内联类参数的方法具有替换了基础字段的类型签名和损坏的名称。这也应该适用于接口方法。但我没有看到任何线索表明 Retrofit 可以解释这一切,即使它尝试过,它也无法做到:无法逆转损坏以发现实际的底层类型。对于库来说,该方法看起来就像采用基础类型的任何其他方法一样,只是名称很奇怪。这意味着您可能会遇到以下问题:

\n
@JvmInline\nvalue class MyId(val raw: String) {\n    override fun toString(): String = "where is your god now?"\n}\n\ninterface MyApiEndpoints {\n    @GET("/fetch/{id}")\n    suspend fun fetch(@Path("id") id: MyId): Response<JsonObject>\n}\n
Run Code Online (Sandbox Code Playgroud)\n

正如文档所@Path解释的:

\n
\n

Retrofit.stringConverter(Type, Annotation[])使用(或,如果没有安装匹配的字符串转换器)将值转换为字符串Object.toString(),然后进行 URL 编码。

\n
\n

在 JVM 级别,fetch将有一个带有String参数的类型签名,并且 Retrofit 生成的实现将如此对待它。因此,它不会调用您的自定义实现toString。(在 Kotlin 中,这成功了,因为 Kotlin 在编译时知道类型并静态分派该方法。)

\n

但在内联类及其包装字段之间的区别并不重要的情况下,您可能可以摆脱它。

\n