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

Coo*_*ind 10 android nullpointerexception gson kotlin

在Kotlin你可以创建一个data class:

data class CountriesResponse(
    val count: Int,
    val countries: List<Country>,
    val error: String)
Run Code Online (Sandbox Code Playgroud)

然后您可以使用它来解析JSON,例如"{n:10}".在这种情况下,你将有一个对象val countries: CountriesResponse,从收到的Retrofit,FuelGson包含这些值:count = 0, countries = null, error = null.

Kotlin + Gson中 - 如何在数据类为null时获取emptyList,您可以看到另一个示例.

当您稍后尝试使用时countries,您将在此处获得异常val size = countries.countries.size::"kotlin.TypeCastException:null不能转换为非null类型kotlin.Int".如果您编写代码并?在访问这些字段时使用,Android Studio将突出显示?.并警告:Unnecessary safe call on a non-null receiver of type List<Country>.

那么,我们应该?在数据类中使用吗?为什么应用程序null在运行时设置为不可为空的变量?

hot*_*key 18

这是因为Gson使用不安全(如在java.misc.Unsafe)实例构造机制来创建类的实例,绕过它们的构造函数,然后直接设置它们的字段.

有关一些研究,请参阅此问答:使用Kotlin进行Gson反序列化,不调用初始化程序块.

因此,Gson忽略了构造逻辑和类状态不变量,因此不建议将它用于可能受此影响的复杂类.它也忽略了setter中的值检查.

考虑一个Kotlin感知的序列化解决方案,例如Jackson(在上面链接的问答中提到)或kotlinx.serialization.

  • 在使用 Gson 时,我也遇到了不可为空字段中的空值问题。因此,我为 Gson 编写了一个包装器来检查此类无效的空值。查看:https://github.com/taskbase/arson/ (2认同)
  • 我使用了 MoshiConverterFactory 并遇到了与 Gson 给我的问题完全相同的问题...... (2认同)

Age*_*t_L 9

JSON 解析器在两个本质上不兼容的世界之间进行转换 - 一个是 Java/Kotlin,具有静态类型和 null 正确性,另一个是 JSON/JavaScript,其中一切都可以是一切,包括null甚至不存在,并且属于“强制”概念根据您的设计,而不是语言。

因此,差距必然会发生,并且必须以某种方式加以处理。一种方法是对最轻微的问题抛出异常(这会让很多人当场生气),另一种方法是即时捏造价值观(这也会让很多人生气,只是稍后)。

Gson 采用第二种方法。它默默地吞噬着缺席的田野;将对象设置为null并将原语设置为0false,完全屏蔽 API 错误并导致下游出现神秘错误。

因此,我建议采用两阶段解析:

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)
Run Code Online (Sandbox Code Playgroud)

是的,这是两倍的工作量 - 但它会立即查明 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()
       )
Run Code Online (Sandbox Code Playgroud)

是的,您可以使用更好的解析器,有更多的选项 - 但您不应该这样做。您应该做的是将解析器抽象出来,以便您可以使用任何解析器。因为无论您今天找到多么先进和可配置的解析器,最终您都将需要它不支持的功能。这就是为什么我将 Gson 视为最低公分母。

有一篇文章解释了在存储库模式的更大上下文中使用(和扩展)的这个概念。