【问题标题】:Is it safe to pass a Kotlin inline class to a Retrofit function将 Kotlin 内联类传递给 Retrofit 函数是否安全
【发布时间】:2021-09-14 19:37:41
【问题描述】:

如果我有以下内联类:

interface StringId {
  val raw: String
}

@JvmInline
value class MyId(override val raw: String) : StringId

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

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

【问题讨论】:

    标签: kotlin retrofit retrofit2


    【解决方案1】:

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

    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-&lt;hashcode&gt;(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 在编译时知道类型并静态分派方法。)

    但是在内联类和它的包装字段之间的区别并不重要的情况下,你只是可能能够摆脱它。

    【讨论】:

    • 感谢您的详细解答!现在想一想,为StringId 提供适配器是否有助于处理任何潜在问题?
    • @Eliezer 我认为更简单的方法是让内联类实现一个接口并将其传递给包装在该接口中的方法,因为这迫使 Kotlin 实际上将底层数据包装在 JVM 中-level 类,如手册前面的asInterface 示例(在我引用的段落之前)。
    • 嗯,在这种情况下,这会破坏内联类对我的好处。我有可以使用的StringId 接口,但我想在 Retrofit 接口中使用内联类来确保将什么 ID 传递给什么调用的类型安全。例如,我会将FooId 传递给syncFoo 函数,并将BarId 传递给syncBar 函数。
    猜你喜欢
    • 1970-01-01
    • 2022-01-19
    • 2018-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多