前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >数据类增加nonNull字段反序列化的坑

数据类增加nonNull字段反序列化的坑

作者头像
bennyhuo
发布2020-02-20 13:15:50
9160
发布2020-02-20 13:15:50
举报
文章被收录于专栏:Bennyhuo

最近一直在忙一些事情,我这篇文章都积压了好几周了。当然是原谅我啊哈哈

1. 数据类增加字段,反序列化 Json 有惊喜?

话说,我们有一个数据类:

代码语言:javascript
复制
data class Person(val name: String, val age: Int)

现在呢,我又有这样的一个 Json 字符串:

代码语言:javascript
复制
{"name":"benny","age":18}

这样我们对这个字符串进行解析并得到 Person 这个类实例,没有毛病。

代码语言:javascript
复制
Gson().fromJson(json, Person::class.java).let(::println)

那么后来,万恶的产品经理该需求啦,说这个 Person 里面还需要有一个公司,也就是我们要改成:

代码语言:javascript
复制
data class Person(val name: String, val age: Int, val company: String)

嗯,到这里似乎也没啥毛病啊。不过不巧,本地之前缓存了一份刚才的 Json,程序重新运行之后试图从这个 Json 解析出一个 Person,程序跑着倒也没什么,只是输出有点儿奇怪:

代码语言:javascript
复制
Person(name=benny, age=18, company=null)

哎?? company 怎么还能等于 null ?为什么会这样?具体原因可以参考我很久之前的视频:Json 数据引发的血案

视频内容

这主要是因为 Gson 通过 Unsafe 这个东西去实例化 Person,所以里面的字段实际根本没有经过有效的初始化。而因为 Json 字符串当中有 name age 这两个字段,所以他俩会在后面通过反射赋值, company 就没人疼没人爱了。

所以你就知道了,Kotlin 里面的字段在这种鬼畜的写法下面也会被突破限制,空类型安全似乎也很无力。因为我们反序列化 Json 的时候既然没有报错,那么后面的代码肯定会安心洗路的用里面的字段:

代码语言:javascript
复制
fun needACompany(company: String){
    ...
}

...
needACompany(person.company)

于是:

代码语言:javascript
复制
Exception in thread "main" java.lang.IllegalArgumentException: Parameter specified as non-null is null

所以我们的问题是,对于这种情况,怎样才能让我们的代码不出现“反常”的情况?

2 委曲求全, 用 Nullable 类型

太可怕了。也就是说我们如果新增字段的话,我们只能将他们定义为 nullable 的类型吗?

首先明确一点的是,定义为 nullable 类型,是一种解决方案,也就是说:

代码语言:javascript
复制
data class Person(val name: String, val age: Int, val company: String?)

使用的过程中就要这样:

代码语言:javascript
复制
person.company?.let(::needACompany)

不过,这个方案可能会让很多人感到不开心。于是有人说,我给这个 company 加个默认值行不行啊,毕竟可以给个空字符串嘛,总比 null 强吧。

3. 徒劳无功,默认参数救不了你

那么定义默认参数怎么定义,当然就是在构造器里面写了

代码语言:javascript
复制
data class Person(val name: String, val age: Int, val company: String = "")

这个默认参数虽然有了,如果真的可以在反序列化 Json 的时候遇到没有 company 字段的情形赋值为空字符串的话,那么我们也不会遇到前面的异常了。

可是它并不会被真正调用。如果你不知道默认参数的原理,那么我建议你反编译字节码看下。实际上只有真正调用这个主构造器的时候才可能去触发默认参数的逻辑, Gson 通过 Unsafe 去实例化的路径根本不会触发默认参数的赋值。

那么还有人说,干脆把 company 定义在类内部好了,就像这样:

代码语言:javascript
复制
data class Person(val name: String, val age: Int){
    val company: String = ""
}

看上去到也还行,不过有个问题,作为数据类, company 字段的地位可比其他两个低多了,不信你去试试 copy 方法、或者试下解构赋值,感觉就是后娘养的。

代码语言:javascript
复制
val (name, age) = person //正确
val (name, age, company) = person //错误,没有 component3

所以这条路不通。

4. 柳暗花明,noArg 的妙用

我们再来理一下,我们的目标其实是要做到:

  1. company 字段定义为 nonNull 类型
  2. 在反序列化 Json 时,如果 Json 中没有这个字段,要赋值为空字符串,也就是要有个默认值

熟悉 Kotlin 数据类的坑的朋友们都知道,NoArg 和 AllOpen 无论如何都是少不了的。不过今天说的这个问题只是简单的应用这俩插件可不行。

我们知道有了 NoArg 插件,编译器会帮我们生成一个无参构造方法,这时候 Gson 就可以通过这个构造来实例化 Person。可是问题还没解决呢,里面的字段还是没有初始化啊。没关系,调用这个默认无参构造的时候会首先调用父类构造,所以我们给 Person 搞一个父类好了:

代码语言:javascript
复制
abstract class PersonCompat

然后在这个默认无参构造调用的时候对可能不存在的字段进行初始化赋值,由于这个操作在前,如果这个字段在 Json 当中存在,那么就用 Json 当中的值,也即不会对正常的逻辑造成影响。

代码语言:javascript
复制
abstract class PersonCompat{
    abstract var company: String

    init {
        company = "默认值"
    }
}
@PoKo
data class Person(val name: String, val age: Int, override var company: String): PersonCompat()

那么这时候再去反序列化刚才那段 Json 的时候,得到的结果如下:

代码语言:javascript
复制
Person(name=benny, age=18, company=默认值)

这里面有几个细节,请大家注意 company 被定义为了可读写变量,而非之前的只读变量; company 在父类中定义为抽象的;父类当中一定要在 init 中赋值。想想这是为什么。

这个方案至少是可行的,从使用的角度来看,也可以达到我们的需求。

不过似乎也看上去比较重,因为引入了一个父类。实际上,从代码设计的角度来看,数据类通常也不需要父类,这个意义上讲,这个方案是可用的。

那么对于新增的字段,我们通常实际上也是要做好兼容处理的,文档之类的必不可少,那么从这个意义上讲,这个方案还可以很清晰的告诉代码维护者哪些字段是做了兼容处理的,非常棒。

不知道大家有没有听说过这个梗,有人反映说为啥 Kotlin 对于 nullable 的字段这么苛刻,每次都写 ?. 感觉很丑啊;官方的人回复说,它就是很丑啊,就是要丑到让你难受然后去用 nonNull 的类型进而避免问题的产生嘛。从这个意义上来讲,设计本身是丑的,写出丑的代码就是理所当然的,警示作用。

5. 小结

如果哪天出个插件可以把主构造器里面的默认参数应用上就好了。嗯,就这样。


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-11-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Kotlin 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 数据类增加字段,反序列化 Json 有惊喜?
  • 2 委曲求全, 用 Nullable 类型
  • 3. 徒劳无功,默认参数救不了你
  • 4. 柳暗花明,noArg 的妙用
  • 5. 小结
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档