为什么Kotlin数据类可以用Gson在非空字段中有空值?

vktxenjb  于 2022-11-06  发布在  Kotlin
关注(0)|答案(2)|浏览(188)

在Kotlin中,您可以创建一个data class

data class CountriesResponse(
    val count: Int,
    val countries: List<Country>,
    val error: String)

然后,您可以使用它来解析JSON,例如,“{n:10}"。在这种情况下,您将拥有一个从RetrofitFuelGson接收的对象val countries: CountriesResponse,它包含以下值:count = 0, countries = null, error = null .
Kotlin + Gson - How to get an emptyList when null for data class中,您可以看到另一个示例。
当您稍后尝试使用countries时,您会在此处收到例外状况:异常错误:如果您编写代码并在访问这些字段时使用?,Android Studio将突出显示?.并发出警告:Unnecessary safe call on a non-null receiver of type List<Country> .
那么,我们应该在数据类中使用?吗?为什么应用程序可以在运行时将null设置为不可为空的变量?

3wabscal

3wabscal1#

这是因为Gson使用了一个不安全的(如java.misc.Unsafe)示例构造机制来创建类的示例,绕过它们的构造函数,然后直接设置它们的字段。
请参阅此问答以了解一些研究:Gson Deserialization with Kotlin, Initializer block not called
因此,Gson忽略了构造逻辑和类状态不变量,因此不推荐将其用于可能受此影响的复杂类。它也忽略了setter中的值检查。
考虑一个支持Kotlin的序列化解决方案,比如Jackson(在上面链接的Q&A中提到过)或kotlinx.serialization

mi7gmzs6

mi7gmzs62#

JSON解析器在两个本质上不兼容的世界之间进行转换-一个是Java/Kotlin,它具有静态类型和空值正确性;另一个是JSON/JavaScript,其中任何内容都可以是任何内容,包括null,甚至不存在,“强制”的概念属于您的设计,而不是语言。
因此,差距是必然会发生的,必须以某种方式加以处理。一种方法是在最小的问题上抛出异常(这会让很多人当场生气),另一种方法是在运行中编造值(这也会让很多人生气,只是稍后)。
Gson采取了第二种方法,它默默地吞噬着缺席的田野;将对象设置为null,将基元设置为0false,从而完全屏蔽API错误,并导致更下游的隐含错误。
因此,我建议使用2阶段解析:

package com.example.transport
//this class is passed to Gson (or any other parser)
data class CountriesResponseTransport(
   val count: Int?,
   val countries: List<CountryTransport>?,
   val error: String?){

   fun toDomain() = CountriesResponse(
           count ?: throw MandatoryIsNullException("count"),
           countries?.map{it.toDomain()} ?: throw MandatoryIsNullException("countries"),
           error ?: throw MandatoryIsNullException("error")
       )
}

package com.example.domain
//this one is actually used in the app
data class CountriesResponse(
   val count: Int,
   val countries: Collection<Country>,
   val error: String)

是的,这是两倍的工作量-但它可以立即查明API错误,并在您无法修复这些错误时为您提供处理这些错误的位置,例如:

fun toDomain() = CountriesResponse(
           count ?: countries?.count ?: -1, //just to brag we can default to non-zero
           countries?.map{it.toDomain()} ?: ArrayList()
           error ?: MyApplication.INSTANCE.getDeafultErrorMessage()
       )

是的,你可以使用一个更好的解析器,有更多的选择--但你不应该这样做。你应该做的是把解析器抽象出来,这样你就可以使用任何解析器。因为无论你现在找到的解析器多么先进和可配置,最终你都会需要一个它不支持的特性。这就是为什么我把Gson作为最低的共同标准。
There's an article,解释了在更大的存储库模式上下文中使用(和扩展)的这个概念。

相关问题