写点什么

Kotlin 使用 lateinit 的一些考虑

作者:子不语Any
  • 2022-11-30
    湖南
  • 本文字数:1881 字

    阅读完需:约 6 分钟

Kotlin使用 lateinit 的一些考虑

使用 lateinit 的初衷

如何看待 lateinit?有的开发者对它敬而远之,特别是使用 lateinit 踩坑之后。因为被 lateinit 修饰的变量,不再接受空安全检查,它更像是一个普通的 Java 变量。也有开发者喜欢尽可能的用它,把 lateinit 作为介于 nonnull 和 nullable 之间的一个状态:对象构造时为 null,在某一个时刻被初始化后一直都是 nonnull,属性的不确定性便减少了。


平时也比较喜欢使用 lateinit 修饰变量。原因之一是 lateinit 属性比 nullable 属性在行为上更可靠。所谓可靠,即其行为是确定的。当调用 lateinit 变量时,此时如果没有被初始化,就会抛出UninitializedPropertyAccessException;若已经初始化,则操作一定会执行。反看 nullable 变量,在任一时刻操作它的时候,它都可能不被执行,因为可空变量在任意时刻都可能被置空。这样在排查问题的时候会造成阻碍。为了减少程序运行的不确定性,我会更倾向使用 lateinit 代替 nullable。


另一个原因是既然 Kotlin 语言提供了这个关键字,那必有它可用之处。

使用 lateinit 的坚持

理性分析完,就要在项目中使用。只要是符合以下条件,会考虑使用 lateinit 来修饰属性:


  • 该属性在对象构造时无法初始化(缺少必要参数),在某个阶段被初始化之后会一直使用。典型的初始化阶段:Activity.onCreate(),自定义模块的 init()

  • 保证对象的调用都在初始化之后

  • 属性无法用空实现代替。


这个策略看起来是没什么问题的,执行的也比较顺利。自测没有问题,测试那边也顺利通过了。但在灰度的期间还是出现了 UninitializedPropertyAccessException


Crash 量也不多,但总还是得解决。Crash 的原因无非就一个:在初始化 lateinit 属性之前调用了该属性。 而解决方案根据不同情况有两种:


  • 是异常路径导致,如 Activity.onCreate() 时数据不正确,需要 finish Activity 不再执行后续初始化代码。此时 Activity 仍然会执行 onDestroy(),而 lateinit 属性没有被初始化。如果 onDestroy() 有对 lateinit 属性的操作,此时就会抛出 UninitializedPropertyAccessException


解决方案:使用 ::lateinitVar.isInitialized 方法,对异常路径的 lateinit 属性进行判断,如果没有初始化则不操作。


对比 nullable 属性:lateinit 属性会 crash,nullable 属性不会,且和 lateinit 属性加了初始化判断的效果一致。这种场景下 nullable 属性表现的更好。


  • 是代码逻辑结构不正确导致,如在某些情况下,上层在调用模块 init() 方法之前,就调用了模块的其他方法。此时抛出 UninitializedPropertyAccessException


解决方案:调整代码调用逻辑,保证调用模块init()方法之前不调用模块的其他方法。


对比 nullable 属性:lateinit 属性会 crash,nullable 属性不会。但 lateinit 属性会把问题暴露出来,而 nullable 属性会把问题隐藏起来,导致问题难以发现和解决。


开发者对 lateinit 的争论也大多源自于此。支持 lateinit 的开发者,是希望代码有更好的逻辑性;反对 lateinit 的开发者,是希望代码有更好的健壮性。就看开发者自己的取舍。

使用 lateinit 的痛苦

理论和实践都完善了,但苦恼的是,UninitializedPropertyAccessException并没有得到高效的解决,而是三头两日时不时的在灰度时冒出来,需要花上一点时间解决,并延长版本灰度的时间。这不是想要的效果。UninitializedPropertyAccessException主要出现这几种场景:


  • 新代码使用了 lateinit 特性,因没有考虑异常路径在测试期间出现 crash;

  • 旧代码重构后对部分属性使用了 lateinit 特性,在复杂的线上环境中出现 crash;

  • 模块内部代码调整/外部调用逻辑调整,如调用时机的调整,导致之前没有问题的代码,在复杂的线上环境中出现 crash。


Kotlin 的 UninitializedPropertyAccessException本质上和 Java 的空指针错误是一样,都是错误的估计此处对象不可能为空导致的。在 Java 中我们通过增加一堆空判断来解决这个问题,Kotlin 可以使用 nullable 对象。


而 lateinit 通过舍弃空安全机制,把空安全交回到开发者手上(就像 Java 那样)。但在实践中,让开发者自己掌控空指针问题,是困难的。(不然 kotlin 中的 空安全机制也不会被提出来)

使用 lateinit 的建议

使用 lateinit 有几点建议:


  1. 充分考虑异常分支的执行情况;

  2. 充分考虑异常时序的执行情况;

  3. 充分考虑代码稳定性,是否容易发生需求变更导致结构调整。


目前依然有典型的 lateinit 适用场景,如Activity.onCreate()初始化的属性。但不要忘了若初始化失败,需要在异常路径onDestroy()上增加::lateinitVar.isInitialized判断。


对于 Fragment,如果在onCreate执行了 finish(),它的异常路径会是onCreateView()onViewCreate()onDestroy()

发布于: 刚刚阅读数: 6
用户头像

子不语Any

关注

If not now,when? 2022-09-17 加入

安卓攻城狮

评论

发布
暂无评论
Kotlin使用 lateinit 的一些考虑_android_子不语Any_InfoQ写作社区