java kotlin lateinit_15. 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 半年以上。而这一段使用 lateinit 的痛点,也让我开始重新思考 lateinit 的收益。

使用 lateinit 的痛苦

理论和实践都完善了,但使我苦恼的是,UninitializedPropertyAccessException并没有得到高效的解决,而是三头两日时不时的在灰度时冒出来,使我被迫打断当前工作,花上一点时间解决,并延长版本灰度的时间。这不是我想要的效果。

UninitializedPropertyAccessException主要出现这几种场景:

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

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

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

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

而 lateinit 通过舍弃空安全机制,把空安全交回到开发者手上(就像 Java 那样)。但在这几个月的实践中,我发现让开发者自己掌控空指针问题,是困难的。

我发现之前我对 lateinit 的思考,缺少了一个很重要的角度:软件工程的角度。代码是不断迭代的,维护者可能不止一个人,而 lateinit 对空指针问题的保护不足,容易让新的空指针问题出现在代码迭代之后。没有从软件工程的角度去看待问题,导致我对代码的规划过于理想,让代码降低了健壮性。现在我想给 lateinit 增加这样一个观点:lateinit 是一个和软件工程相悖的特性,它不利于软件的健康迭代。

使用 lateinit 的建议

如果你仍想使用 lateinit,那么我建议:

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

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

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

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

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

相关

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
能将以下kotlin代码转换成java吗 package com.blog.demo41 import android.os.Bundle import android.util.Log import android.view.View import android.widget.Button import com.blog.AbstractLoggerActivity import com.blog.R import com.blog.demo39.TAG import com.blog.support.logger.Logger import java.io.BufferedReader import java.io.IOException import java.io.InputStream import java.io.InputStreamReader class StrokeOrderActivity : AbstractLoggerActivity() { var svgSix: String? = null var svgOne: String? = null lateinit var strokeOrderView1: StrokeOrderView lateinit var strokeOrderView2: StrokeOrderView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_stroke_order_layout) initLoggerLayout() strokeOrderView1 = findViewById(R.id.stroke_order_view1) strokeOrderView2 = findViewById(R.id.stroke_order_view2) findViewById<Button>(R.id.btn_load_svg_six).setOnClickListener { val name = "张.json" // 需要将 svg.json 放在 assets 或特定路径下 svgSix = loadSvgFromAssets(name) showTips("加载$name ->$svgSix") svgSix?.let { showTips("start draw -> $name") strokeOrderView1.setStrokesBySvg(it) } } findViewById<Button>(R.id.btn_load_svg_one).setOnClickListener { val name = "張.json" svgOne = loadSvgFromAssets(name) showTips("加载$name ->$svgOne") svgOne?.let { showTips("start draw -> $name") strokeOrderView2.setStrokesBySvg(it) } } } private fun loadSvgFromAssets(name: String): String? { try { assets.list("data")?.let { for (s in it) { if (name == s) { Log.d("zuo", "svgName-> $s") return loadSvgJson("data/$s") ?: "NULL" } } } } catch (e: IOException) { e.printStackTrace() } return null } private fun loadSvgJson(file: String): String? { var reader: BufferedReader? = null var inputStreamReader: InputStreamReader? = null try { val inputStream: InputStream = assets.open(file) inputStreamReader = InputStreamReader(inputStream) reader = BufferedReader(inputStreamReader) var line: String? val entity = java.lang.StringBuilder() while (reader.readLine().also { line = it } != null) { entity.append(line) } return entity.toString() } catch (e: java.lang.Exception) { e.printStackTrace() } finally { try { inputStreamReader?.close() reader?.close() } catch (e: IOException) { e.printStackTrace() } } return null } private fun showTips(str: String) { Log.d(TAG, str) Logger.i(TAG, str) } }
06-05
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值