切換語言為:簡體
作為程式設計師,你知道表示式和語句的區別嗎?

作為程式設計師,你知道表示式和語句的區別嗎?

  • 爱糖宝
  • 2024-11-11
  • 2030
  • 0
  • 0

前言

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 表示式中的語句

我們回過頭來看問題, 首先從 HandlerpostDelayed 這個方法出發

  • 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 表示式 {} 。對於 HandlerpostDelayed 方法來說,他需要的只是一個函式式介面,並不一定是 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

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.