SickWorm的博客

4. Kotlin 变量声明和变量状态设计

Kotlin  ·  

本篇文章将会介绍如何通过正确的变量状态设计来达到简化代码逻辑的效果。

本篇并不是针对 Kotlin 的语言特性介绍,但它比语言特性更为重要。上一篇文章讲的是空安全特性,它允许你方便的处理对象可能为空的情况。但他价值更大的另一面在于,Kotlin 可以声明不可能为空的对象

1. 非空类型

对象不可能为空意味着程序复杂度的降低。而且这不是一般的降低,因为我们开发过程很多时候都是在处理“这个变量可能为空”的情况。在 Java 的环境里,我们出于对调用的 SDK 的不信任,总是要去判断以下是否为空,以保平安,这样处理的代价就是,增加了大量的异常分支代码。如果一个变量他永远都不可能为空,那其实是一件很快乐的事!一个对象可能的状态减少了,程序逻辑会变得更简单清晰,代码的可维护性会大大的提高。我们应该尽量将一个变量声明为非空类型。

Java 提供了 @NonNull 和 @Nullable 注解来满足对象状态的空设计。但由于默认只会产生警告级别的提示(相信我,很多程序员不看 warning),以及使用的繁琐,它最终落得和 final 一样的使用频率。

你很可能会担心非空类型会带来内存泄漏。因为在 Java 很多释放操作都会将引用的变量设置为空,这是个很常见的防止内存泄漏的办法。但代价是将程序状态复杂化。我们确实应该慎重考虑一个变量是否可以一直被持有,但大部分情况我们是可以不用担心的。如 Android 开发基本只要考虑 Activity 是否间接被单例这样生命周期过长的对象持有即可。我还依稀记得刚学 Android 的时候,有些网上教程还会教你在 onDestroy 的时候将 onClickListner 设置为 null 防止内存泄漏。。

2. lateinit

说到尽量声明为非空类型,有人就会提出质疑了:非空类型说来简单,但部分依赖外部调用完成初始化的变量,无法声明为非空类型啊?Activity 的初始化,就是通过 onCreateView 回调初始化的,各种 UI 对象只能在 onCreate 回调的时候被赋值。

针对这种情况,可以使用 Kotlin 的 lateinit 关键字。lateinit 人如其名,它表示这个对象会在稍后被初始化。它还有两条限制:

  1. 无法用 val 修饰,只能用 var 修饰;
  2. 必须为非空类型。

1 很好理解,val 意义是声明后无法再被重新赋值,就和 final 一样。而 lateinit 变量要在稍后才被赋值,所以必须是 var。var 也意味着 lateinit 变量可以被多次赋值,可被多次赋值可能是你想要的,也有可能是你不想要的。

2 的话,设想一下,如果是可空类型,也没必要用 lateinit 了,直接初始化为 null 即可。所以 2 也是合理的。

如果一个变量被声明为 lateinit,你可以不用在声明时初始化它,在任意地方把它当作非空类型直接使用。注意了,此时如果你在初始化这个变量前就使用了该变量,则会丢出一个 RuntimeException:

UninitializedPropertyAccessException: lateinit property has not been initialized

意思就是你还没初始化这个变量就使用它了。所以使用 lateinit 关键字,就需要你自己保证调用顺序,保证调用时变量已经被初始化,Kotlin 不再帮你把关了。这看起来像是一个把 Kotlin 空安全废掉,退化为原来 Java 的无空检查的行为。这样就很没意思了,但其实不是这样,lateinit 有他特有的表意,即:这个变量在稍后会被初始化,且以后都不再为空。以后不再为空即是他和可空变量的区别,从状态复杂度来看,lateinit 变量是介于非空变量和可空变量之间的。

使用 lateinit 是一个有风险的事情,因为非空的条件变复杂了(初始化后才是非空)。如果你不能保证所有调用都在赋值后发生,则不应使用它。但对于 Activity 的 onCreate 这种简单的场景,还是建议使用 lateinit 的。但需要注意一点:

如果 Activity 在 onCreate 的时候初始化失败了,你需要弹窗或直接 finish 的时候,此时你的 lateinit 变量可能没有被赋值,而 Activity 仍会执行 onStart onResume onDestroy 这些回调。这种情况就是“没法保证调用前变量已经初始化”的情况了。

这个时候你可以选择将变量声明为可空类型。也可以用 lateinit 变量专有的判断方法::xxx.isInitialized在关键路径进行判断,比如 Activity onCreate finish 掉的话,关键路径就只剩 onDestroy了(Fragmet 还有 onCreateView 和 onViewCreated)。但相比这两种办法,我更建议你思考,这样复杂的情景是不是我想要的,设计是否能够简化?因为正确设计的程序的状态应该是简单清晰的。

3. 空对象模式(Null Object Pattern)

其实相对于 lateinit,我更喜欢空对象这个设计模式。它没有 lateinit 引入的风险,是一种更简单的状态。空对象就是拥有这个类默认实现的对象。对于数据类来说,它的空对象可能所有成员变量都是0,false,长度为0的字符串;对于带方法的类来说,它的空对象可能是所有方法都是空的,可以调用但没有任何效果。这样一个空对象,它可以帮你代替 null,临时顶替正常实现,直到被重新赋值。同样是初始化异常,lateinit 可能会崩溃,而空对象最多是表现异常。

可参考:https://en.wikipedia.org/wiki/Null_object_pattern

4. final

除了 Kotlin 的非空类型/可空类型,val/var(即 Java 的 final 关键字)也是减少变量状态的利器。而且它比非空类型更彻底,非空类型只是不允许这个变量变为 null,val 直接不允许变量重新被赋值!声明为 val 的变量状态可能性更少,并发竞争的问题都没有了。

变量状态设计原则

经过上面的变量状态介绍,我们按照变量状态从简单到复杂的顺序,可以得到一个变量状态声明的优先级:

  1. 声明为 val 变量,无法满足再考虑 var
  2. 声明为非空变量
  3. 无法满足声明时赋值,优先考虑赋值为空对象
  4. 无法满足空对象,看看是否可以用 lateinit
  5. 声明为可空变量

版权所有,转载请注明出处:
https://sickworm.com/?p=1784