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



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

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

然后,您可以使用它来解析 JSON,例如"{n:10}"。在这种情况下,您将有一个对象val countries: CountriesResponse,从RetrofitFuelGson接收,其中包含以下值:count = 0, countries = null, error = null

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

当你以后尝试使用countries时,你会在这里得到一个例外:val size = countries.countries.size:"kotlin。TypeCastException:null 不能强制转换为非 null 类型 kotlin。国际"。如果您在访问这些字段时编写代码并使用?,Android Studio 将突出显示?.并警告:Unnecessary safe call on a non-null receiver of type List<Country>

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

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

有关一些研究,请参阅此问答:Gson 反序列化与 Kotlin,未调用初始值设定项块。

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

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

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视为最低公分母。

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

最新更新