【问题标题】:Implementing generic method in interface that uses implementors class在使用实现类的接口中实现泛型方法
【发布时间】:2021-11-30 19:08:18
【问题描述】:

我想要一个父接口/抽象类上的方法,该方法利用泛型方法传入实现类的类。

interface Domain {
    fun toJSON(): String { return Json.encodeToString(this) }
}

@Serializable
class User: Domain {
    val a: Int
}

这不起作用,因为Json.encodeToString 不知道“this”的类别。

@Serializable 似乎实现了KSerializer,所以理论上我可以要求域从它继承,但该接口是模板化的。并且标记实现类@Serializable 似乎直到编译时才实现KSerializer,因此会产生错误。

如何实现这个toJSON() 方法或告诉Domain 它的实现者必须是@Serializable / KSerializer

我也试过了:

interface Domain<T> {
    fun toJSON(): String { return Json.encodeToString(this) }
}

@Serializable
class User: Domain<User> {
    val a: Int
}

但这会导致:

kotlin.IllegalStateException: Only KClass supported as classifier, got T

所有这一切的另一个复杂因素是我试图在 KMM 中执行此操作。

【问题讨论】:

  • 添加扩展功能怎么样?
  • @sidgate 感谢您的想法。我添加了有趣的 KSerializer.toJSON(),但它需要 KSerializer 这让我回到上述问题。

标签: kotlin generics kmm


【解决方案1】:

它不起作用,因为encodeToString() 在编译时解析类型,而不是在运行时解析(它使用具体化类型)。

也许有更好的方法,但您可以通过手动获取序列化程序,在运行时解析类型来做到这一点:

fun toJSON(): String {
    val serializer = Json.serializersModule.serializer(this::class.createType())
    return Json.encodeToString(serializer, this)
}

请注意,这仅适用于 JVM。如果您需要在 KMM 中执行此操作,请注意多平台反射很复杂和/或需要一些时间才能成熟。 kotlinx.serialization 提供了一种方法来做到这一点,但是有关于平台之间可能存在不一致行为的警告:

@OptIn(InternalSerializationApi::class)
fun toJSON(): String {
    @Suppress("UNCHECKED_CAST")
    val serializer = this::class.serializer() as KSerializer<Any?>
    return Json.encodeToString(serializer, this)
}

【讨论】:

  • 这似乎是一个可行的解决方案。我忽略了我试图从 KMM 内部执行此操作。我相信,不幸的是,在“公共”区域内,我无法访问 createType 方法(除非我做错了什么)。
  • 我更新了答案。
【解决方案2】:

Json.encodeToString 使用一个具体化的泛型来访问参数的类。这意味着它使用参数的声明类型(在编译时已知),这里是Domain

最简单的方法是使用您自己的通用具体化扩展函数来更精确地捕获接收器的声明类:

interface Domain

inline fun <reified T : Domain> T.toJSON(): String = Json.encodeToString(this)

@Serializable
data class User(
    val name: String,
    val age: Int,
): Domain

fun main() {
    val user = User("Bob", 35)
    println(user.toJSON())
}

但是请注意,这会遇到同样的问题:如果您尝试在使用 Domain 类型声明的变量上使用此扩展,它仍然不会尝试访问运行时类型:

val user: Domain = User("Bob", 35)
println(user.toJSON()) // still a problem here

如果你想实际使用动态类型,你可以像@broot 提到的in his answer 那样动态访问序列化器。

【讨论】:

  • 对于我的特定用例,这可能就足够了。但是,我没有提到我正在尝试从 KMM 执行此操作。出于某种原因,toJSON 方法没有暴露给 iOS。我会试着找出原因。
【解决方案3】:

@Serializable 似乎实现了 KSerializer

这不是真的。 @Serializable 是一个注解,它指示注解处理器生成(除其他外)一个serializer() 方法(用于伴生对象),该方法为此类返回一个KSerializer 的实例。它不会为类本身添加新接口,因此您无法告诉类型系统它应该检查什么。

如果找不到序列化程序,您将收到运行时错误:

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

class HasNoSerializableAnnotation

fun main() {
    // Compiles fine, but will throw runtime exception
    // kotlinx.serialization.SerializationException: Serializer for class 'HasNoSerializableAnnotation' is not found.
    Json.encodeToString(HasNoSerializableAnnotation()) 
}

这是有意的,因为可以在没有插件生成的序列化程序的情况下序列化类的实例(即使您没有手动将自定义序列化程序作为另一个参数传递给 encodeToXXX 方法 - 通过在 serializersModule 中传递序列化程序(作为上下文) )。

我如何实现这个 toJSON() 方法或者告诉 Domain 它的实现者必须是@Serializable

没有办法做到这一点。你可以拥有这个 API,但它很容易出现运行时错误:

interface Domain
inline fun <reified T : Domain> T.toJSON() = Json.encodeToString(this)

我如何实现这个 toJSON() 方法或者告诉 Domain 它的实现者必须是 KSerializer

实际上,这里不需要整个 KSerializer - 只是它的序列化部分:

interface Domain
inline fun <reified T : Domain> T.toJSON(serializer: SerializationStrategy<T>) = 
    Json.encodeToString(serializer,this)

【讨论】:

  • 这似乎有效;它编译。我没有提到我正试图从 KMM 做到这一点。出于某种原因,toJSON 方法没有暴露给 iOS。我会试着找出原因。
  • Objective-C(以及 Swift)互操作中尚不支持内联类 (kotlinlang.org/docs/native-objc-interop.html#unsupported)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多