Kotlin多变量判空

Kotlin多变量判空

二月 09, 2022

version 0.1 2022年02月09日20:48:26

[TOC]

背景:

最近梳理了一下自己远古的Kotlin业务代码,在一个Kotlin方法实现里遇到了两个变量连续判空的具体写法引发的一个思考。系统整理一下,当做一个有意思的思考点。如果对于Java与Kotlin单变量判空区别已经熟悉,可直接跳到多变量判空部分。

注: 为了方便说明这次的问题,下文将使用编译器截图来突出标明。

单变量的判空

Kotlin的判空编译期检查

先从熟悉的位置入手,日常开发中判空是无处不在的,为了维护健壮性很多时候都要引入判空。对于判空的检查,Kotlin 在编译期提供了更严格安全的检查,就是线程安全检查。
假设我们分别有一个成员变量与局部变量namelocalName进行非空判断并打印长度。
Java-单变量判空

这是我们很常见的判空情况,在Java中很常见。那么如果我们照搬过来写Kotlin 会是怎样?
Kotlin-单变量判空

可以看到对于成员变量的判空,Kotlin在编译期阶段给出了报错提示。
Kotlin-成员变量判空报错

看提示可以知道,Kotlin对于可变的变量是有线程安全检查的,也就是说之前很多时候Java的这种判空都是非线程安全的。多数时候Android开发其实那样写也没有事情,但Kotlin会更加明确的让你知道,提高我们的编程意识。

在初学时候没有想过太多,想当然那么我就利用!!强制非空来判断,以此达到目的。

Kotlin-强制非空

这样编译器是不报错了,但是我们真正的解决提示说的问题了吗?很遗憾并没有解决线程安全的问题,如果涉及多线程那么我们对于这个变量的保护措施就是不够的。

线程安全的判空

那么如何来保证线程安全的判空?简单直观的一种方式就是用局部变量来进行判空保护。先贴出写法:

Java-线程安全判空

对于此写法是利用了局部变量的特性来维护值的拷贝,不被其他线程修改的特性。

针对于此Kotlin才有了诸如?.run、apply、let、with等特性来辅助我们进行快速的线程安全的非空判断。

Kotlin-线程安全单变量判空

Kotlin-let源码

以上类似的问题如果展开篇幅也比较多,虽然偏八股文一些但本质上还是会对我们编程安全有很大的启发与指导意义的。详细介绍与思考可以参考下面文章,本篇文章不再展开。
Java的值传递和引用传递,你真的搞清楚了吗?

多变量判空

回到这篇文章的记录初衷,先看一下原来的代码。

Kotlin-多变量判空原始
这里可以看到首先是非线程安全,如果在特定场景单线程也罢,顶多是一个let的语法糖问题。

Kotlin-多变量判空嵌套

多变量嵌套臃肿问题

但是这样做在面临多变量时就会显得臃肿嵌套变多,而且甚至还不得不这样做。那么能不能简化一下?我也搜索了一下百度,那么看到常见的标题符合我的预期,方法是这样:

Kotlin-多变量判空资料方法

乍一看,符合预期,赶紧试试看。
(这里有一个有意思的点,就是我发现项目里之前一位同事已经在公共的Kotlin扩展函数库里添加了这个方法,甚至欣喜若狂原来英雄所见略同)

Kotlin-判空方法依旧不行

这一刻突然有了一些疑惑,为什么还是需要判空?然后仔细看了一下,这方法障眼法太强,本质上还是因为判断时用了函数来屏蔽成员变量,使用时依旧没用局部变量导致了这个判断几乎等同于上面Java原生写法,甚至可以说更繁琐了。

简易但明确的一种解决方案

在Kotlin社区的讨论中,发现这个问题在16年时候社区里就有人提出来过,并且下面的讨论也都很精彩对自己也都是个启发,其中不乏就有上面那个网络传闻的解决方案的原版(或许)。

Kotlin社区提问

Kotlin社区答案之一
点开评论,早就有前辈提出来同样的问题,这个方法仍旧不可行。

其中的讨论都特别有意思,有人类比Swift与Groovy等语言特性以此来希望官方支持等等。

最后我才用了一种更“笨”,但我认为更实用的解决方法。Kotlin社区答案原贴

1
2
3
4
fun <R, A, B> withNoNulls(p1: A?, p2: B?, function: (p1: A, p2: B) -> R): R? = p1?.let { p2?.let { function.invoke(p1, p2) } }
fun <R, A, B, C> withNoNulls(p1: A?, p2: B?, p3: C?, function: (p1: A, p2: B, p3: C) -> R): R? = p1?.let { p2?.let { p3?.let { function.invoke(p1, p2, p3) } } }
fun <R, A, B, C, D> withNoNulls(p1: A?, p2: B?, p3: C?, p4: D?, function: (p1: A, p2: B, p3: C, p4: D) -> R): R? = p1?.let { p2?.let { p3?.let { p4?.let { function.invoke(p1, p2, p3, p4) } } } }
fun <R, A, B, C, D, E> withNoNulls(p1: A?, p2: B?, p3: C?, p4: D?, p5: E?, function: (p1: A, p2: B, p3: C, p4: D, p5: E) -> R): R? = p1?.let { p2?.let { p3?.let { p4?.let { p5?.let { function.invoke(p1, p2, p3, p4, p5) } } } } }

而没用varargs是因为泛型的问题,提供到第五个参数的重载应该也足够了。本质上虽然我依旧没有很好消除嵌套的方式,但是利用语法糖特性依旧可以做到在调用处消除嵌套的使用现象。如果后续知道并了解后,我会继续更新。

改造后代码:
Kotlin-多变量判空改造后代码

更新日志:

版本 时间 说明
version 0.1 2022年02月09日20:48:26 初版整理