前言
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