前言
Kotlin 相比 Java 语言提供了非常多的语法糖,使得日常编码的时候非常灵活,可以借助语法糖非常高效的完成繁琐的工作。但是,如果对这些语法糖的理解不够深入,就会掉进坑里遇到奇奇挂怪的 bug,本篇总结由于表达式和语句差异导致的一个问题。
表达式 && 语句
我们看一个例子,🌰
private val task1 = Runnable { Log.d(TAG, "task1 run() called") } private val task2 = Runnable { Log.d(TAG, "task2 run() called") } handler.postDelayed({ task1 }, 1000) // ① handler.postDelayed(task2, 2000) // ②
以上代码逻辑很简单,就是通过大家熟悉的 Handler
延迟触发一个任务。
首先,这两种写法从语法上讲都是没有问题的,IDE 并没有报错甚至没有任何 warning
。 既然看起来没有差异,那么代码执行后 ① 和 ② 这两处的都会有日志输出吗?如果不是的话,是哪一行没有?如果你非常确定的知道答案并且了解原因,那么后面的内容可以不用看了,节省时间可以去干点别的。
实际运行结果是,只有 ② 处会执行 task2 , task1 并不会被执行。
2024-11-04 18:59:11.502 30324-30324 DuDuActivity_TAG I start call 2024-11-04 18:59:13.503 30324-30324 DuDuActivity_TAG D task2 run() called
task1 为什么没有被执行呢?我们再加点日志
handler.postDelayed({ Log.i(TAG,"111") task1 Log.i(TAG,"222") }, 1000)
运行结果
2024-11-04 19:00:59.201 30801-30801 DuDuActivity_TAG I start call 2024-11-04 19:01:00.204 30801-30801 DuDuActivity_TAG I 111 2024-11-04 19:01:00.204 30801-30801 DuDuActivity_TAG I 222 2024-11-04 19:01:01.205 30801-30801 DuDuActivity_TAG D task2 run() called
可以看到其实整段代码已经生效了,但是 task1 这个 Runnable 并没有被触发,这是为什么呢?
其实这个问题的根源要从一个最基本的概念出发去解释,编程语言的表达式和语句。大学计算机课程的入门教材,其实都会提这两个概念,只不过可能都是一笔带过,毕竟大部分情况下我们不需要区分二者,因为我们在编码的时候 IDE 会自动帮我们纠正错误的用法。
语句和表达式的区别在于,表达式有值,并且能作为另一个表达式的一部分使用;而语句总是包围着它的代码块中的顶层元素,并且没有自己的值。
直接看概念似乎毕竟抽象,我们再举个例子 🌰
fun max(a: Int,b: Int) = if (a > b) a else b
在 Kotlin 中 if/when 都是表达式,是有返回值的。因此,可以直接作为方法的返回值。但是在 Java 中,所有的控制结构都是语句,因此不能像上面这么用, 而只能像下面这么实现。
private int max(int a, int b) { if (a > b) { return a; } return b; }
当然,for 循环依然是语句,无论在 Java 还是 Kotlin 中都不能当做表达式使用。
正是因为 IDE 提供这样的语法提示,使得我们无形中忽略了表达式和语句的差异,在 IDE 中把语句和表达式混用之后,根据错误提示在大脑中强行记住了代码不能这些写,得换另一种方式写这样一个概念。久而久之,所有的内容归结为代码,无所谓表达式和语句。
Lambda 表达式中的语句
我们回过头来看问题, 首先从 Handler
的 postDelayed
这个方法出发
postDelayed
public final boolean postDelayed(@NonNull Runnable r, long delayMillis) { return sendMessageDelayed(getPostMessage(r), delayMillis); }
首先 postDelayed
方法接受一个 Runnable
类型的变量,因此我们一开始的例子中②处的调用是标准实现,肯定没有问题。
Runnable
@FunctionalInterface public interface Runnable { public abstract void run(); }
同时 Runnable
这个接口有 @FunctionalInterface
这个注解。FunctionalInterface(函数式接口)是 Java 8 引入的一个概念,它是一个只有一个抽象方法的接口。这个特性使得函数式接口非常适合用作 Lambda 表达式的类型,因为 Lambda 表达式可以作为实现函数式接口的匿名类。
因此,我们可以直接传入一个 Lambda 表达式 {}
。对于 Handler
的 postDelayed
方法来说,他需要的只是一个函数式接口,并不一定是 Runnable 这个类型的接口。此时,postDelayed 执行的是这个表达式。
handler.postDelayed({ Log.i(TAG,"111") task1 Log.i(TAG,"222") }, 1000)
而在这个 Lambda 表达式中,所有的内容都是语句,task1 此时也只是语句,相当于只是声明了一个 Runnable 类型的变量,因此自然不会有结果,想要让 task1 执行,也很简单。
handler.postDelayed({ Log.i(TAG,"111") task1.run() Log.i(TAG,"222") }, 1000)
主动调用 task 这个 Runnable 的 run 方法即可。再看一下结果
2024-11-04 19:04:51.450 31297-31297 DuDuActivity_TAG I start call 2024-11-04 19:04:52.453 31297-31297 DuDuActivity_TAG I 111 2024-11-04 19:04:52.453 31297-31297 DuDuActivity_TAG D task1 run() called 2024-11-04 19:04:52.453 31297-31297 DuDuActivity_TAG I 222 2024-11-04 19:04:53.452 31297-31297 DuDuActivity_TAG D task2 run() called
可以看到这输出已经符合预期了。
因此我们可以确定,在Kotlin 中赋值操作是语句 ,在 Lambda 中只能写语句,因为 Lambda 表达式本身是有返回值的(哪怕是 void,在 Kotlin 中是 Unit),如果其内部还有表达式的话,表达式自身的返回值就没有意义了,或者说会有歧义。因此,从语法上被禁止了。 Lambda 表达式就像一个包含返回值的代码块,让编程语言更灵活了。
Java 中的赋值语句
为了加深理解,我们再来对比一下 Java 语言中类似的情况又会发生什么。
private Runnable task = () -> Log.d("tag", "run() called");
可以看到在 Java 中这种写法会报错,原因和在 Kotlin 中一样,Lambda 表达式内不允许有表达式,只能是语句,而在 Java 中赋值操作是表达式, 因此天然杜绝了这个隐蔽的 bug。
总结
表达式有返回值,语句没有。同时在 Java 和 Kotlin 中,语句和表达式的有会呈现出很多差异。Kotlin 中更多的内容变成了表达式,这样使得高阶函数,即方法作为参数这样的特性让编码更加灵活,但是如果我们对一些特性的了解不够熟悉的话,就会引入一些非常隐蔽的 Bug。
作者:IAM四十二
链接:https://juejin.cn/post/7436004984951832588