扩展函数与高阶函数在序列化中的应用:Gson/Moshi的扩展函数、Kotlin Serialization的DSL配置、自定义序列化器
序列化这件事,说白了就是把对象变成字符串,再变回来。每个Android开发者都跟它打过交道。但你可能没注意到,Kotlin的扩展函数和高阶函数,能让序列化代码变得特别优雅。
我个人习惯把序列化相关的扩展函数放在一个单独的SerializationExt.kt文件里。这样找起来方便,也方便团队其他人复用。今天我们就聊聊怎么用扩展函数和高阶函数,把Gson、Moshi和Kotlin Serialization用出花来。
Gson的扩展函数封装
先说说Gson。这玩意儿大家太熟了,但每次写Gson().fromJson(json, MyClass::class.java)是不是有点烦?
我在项目中遇到过一个问题:团队里有人忘了传TypeToken,导致泛型信息丢失,运行时直接崩了。后来我写了个扩展函数,把这个问题彻底解决了。
// 基础扩展函数
inline fun <reified T> Gson.fromJson(json: String): T {
return fromJson(json, object : TypeToken<T>() {}.type)
}
// 带错误处理的版本
inline fun <reified T> Gson.safeFromJson(json: String): Result<T> {
return try {
Result.success(fromJson(json, object : TypeToken<T>() {}.type))
} catch (e: Exception) {
Result.failure(e)
}
}
// 使用示例
data class User(val name: String, val age: Int)
val gson = Gson()
val json = """{"name":"张三","age":25}"""
// 以前你得这么写
val user1 = gson.fromJson(json, User::class.java)
// 现在只需要
val user2: User = gson.fromJson(json)
核心思路:利用reified关键字和内联函数,在编译期保留泛型信息。这样就不用手动传TypeToken了。
你想想看,每次少写几个字,项目大了能省不少时间。而且safeFromJson返回Result类型,调用方可以用onSuccess和onFailure处理,比try-catch优雅多了。
Moshi的自定义适配器与扩展函数
Moshi比Gson更现代,对Kotlin支持更好。但它也有个痛点:自定义适配器写起来有点啰嗦。
嗯,这里要注意。Moshi的@ToJson和@FromJson注解虽然好用,但每个适配器都得单独写一个类。我建议用扩展函数+高阶函数来简化这个过程。
// 用高阶函数创建适配器
inline fun <reified T> Moshi.Builder.addAdapter(
noinline toJson: (T) -> String,
noinline fromJson: (String) -> T
): Moshi.Builder {
val adapter = object : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader): T? {
return fromJson(reader.nextString())
}
override fun toJson(writer: JsonWriter, value: T?) {
value?.let { writer.value(toJson(it)) }
}
}
return add(T::class.java, adapter)
}
// 使用示例
data class Money(val amount: BigDecimal, val currency: String)
val moshi = Moshi.Builder()
.addAdapter<Money>(
toJson = { "${it.amount}${it.currency}" },
fromJson = {
val parts = it.split(Regex("(?<=\\d)(?=\\D)"))
Money(BigDecimal(parts[0]), parts[1])
}
)
.build()
小技巧:如果序列化逻辑比较复杂,可以把toJson和fromJson抽成顶层函数,这样测试起来更方便。
我曾经在一个金融项目里用这种方式处理金额序列化。团队里十几个模块,每个模块的金额格式都不一样。用这个扩展函数,每个模块只需要传自己的转换逻辑就行,适配器代码完全复用。
Kotlin Serialization的DSL配置
Kotlin Serialization是官方推荐的方案。它的DSL配置能力特别强,但很多人只用了@Serializable注解,没发挥出全部威力。
说白了,你可以用DSL来配置序列化行为,而不是写一堆注解。
// 自定义序列化模块
val myModule = SerializersModule {
// 配置多态序列化
polymorphic(Any::class) {
subclass(User::class)
subclass(Admin::class)
}
// 配置上下文序列化器
contextual(LocalDate::class) {
LocalDateSerializer
}
// 配置默认值
@Serializable
data class Config(
val timeout: Long = 5000,
val retryCount: Int = 3
)
}
// 使用DSL配置Json实例
val json = Json {
prettyPrint = true
ignoreUnknownKeys = true
coerceInputValues = true
serializersModule = myModule
// 自定义编码器
encodeDefaults = true
explicitNulls = false
}
关键点:SerializersModule的DSL配置,让你可以在一个地方集中管理所有序列化规则。比分散的注解好维护得多。
我记得有一次接手一个老项目,里面到处都是@SerialName和@Transient注解,改一个字段名要改十几个地方。后来我用SerializersModule统一配置,代码量直接减半。
自定义序列化器实战
有些场景,内置的序列化器搞不定。比如你需要把枚举序列化成中文,或者把时间戳转成特定格式。这时候就得自己写序列化器。
我建议用高阶函数来封装序列化器的创建过程,这样能省不少重复代码。
// 用高阶函数创建序列化器
inline fun <reified T> customSerializer(
crossinline serialize: (T, JsonEncoder) -> Unit,
crossinline deserialize: (JsonDecoder) -> T
): KSerializer<T> = object : KSerializer<T> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("Custom${T::class.simpleName}")
override fun serialize(encoder: JsonEncoder, value: T) {
serialize(value, encoder)
}
override fun deserialize(decoder: JsonDecoder): T {
return deserialize(decoder)
}
}
// 使用示例:自定义枚举序列化
enum class Status(val label: String) {
ACTIVE("启用"),
INACTIVE("禁用"),
PENDING("待审核")
}
val StatusSerializer = customSerializer<Status>(
serialize = { status, encoder ->
encoder.encodeString(status.label)
},
deserialize = { decoder ->
val label = decoder.decodeString()
Status.values().first { it.label == label }
}
)
@Serializable
data class User(
val name: String,
@Serializable(with = StatusSerializer::class)
val status: Status
)
注意:自定义序列化器一定要处理好异常情况。比如上面的deserialize里,如果找不到匹配的枚举值,应该抛出一个有意义的异常,而不是直接崩掉。
你可能会问:为什么不直接用@SerialName?嗯,如果枚举值很多,或者标签需要从配置文件读取,用自定义序列化器更灵活。我在一个多语言项目里就用这种方式,枚举的标签跟着语言配置走,前端完全不用改代码。
知识体系总览
下面这张图总结了扩展函数和高阶函数在序列化中的核心应用场景:
从这张图能看出来,三种方案的核心思路是一样的:用扩展函数减少重复代码,用高阶函数抽象变化的部分。Gson侧重泛型保留,Moshi侧重适配器复用,Kotlin Serialization侧重DSL配置。但底层逻辑都是函数式编程的那一套——把行为参数化。
好了,这一章的内容就到这里。序列化看起来是个小问题,但处理不好会引发线上bug。用扩展函数和高阶函数把这些逻辑封装好,能省心不少。