《Day 02:第一個 suspend function,暫停到底暫停了什麼》 留下一個還沒解開的疑問。suspend function 暫停的時候,一定需要某個東西,把「暫停當下執行到哪一行、接下來要做什麼、區域變數的值是什麼」這些資訊完整記住,恢復執行時才知道該從哪裡接起。今天要正式打開這個黑盒子,把它的名字說清楚。
先用一句話重述問題:suspend function 暫停的當下,執行位置與變數狀態必須被完整保留,之後才可能準確接續執行,而這件事目前還沒有答案。
答案是,Kotlin 編譯器會把每一個 suspend function 轉換成內部攜帶一個 Continuation 物件的形式,這個物件代表暫停當下「接下來該做什麼」的延續。今天要定案這個詞:Continuation,全系列後續一律使用這個英文詞,不另外翻譯。它會跟 suspend function 一樣,反覆出現在接下來的每一篇文章裡。
如果把 Continuation 想成「一個知道接下來要做什麼的回呼函式」,方向上是接近的。《Day 01:為什麼你需要協程,從一個會卡住主執行緒的下載函式說起》 提到的 Callback 手法,正是開發者手動模擬「接下來要做什麼」這件事的一種笨拙方式,先寫好結果出來之後要執行的邏輯,塞進一個函式裡,交給某個非同步操作,等結果到了再呼叫它。
關鍵差異在這裡:Callback Hell 之所以難維護,是因為這些回呼函式是開發者手寫、手動巢狀組合出來的,一層包一層,程式碼被異步流程扭曲了原本的形狀。Continuation 完全相反,它是編譯器在編譯期自動產生的,開發者寫的仍然是看起來像同步的程式碼,完全不需要手動組合任何回呼結構。這正是協程比手寫 Callback 優越的關鍵之一:讀者不用再自己拼湊「等一下要接著做什麼」這件事,這份工作被搬到編譯器身上完成了。
也因為這樣,Continuation 不是開發者要自己動手處理的物件。日常寫協程程式碼時,幾乎不會直接看到或操作它,它不會出現在你熟悉的函式呼叫鏈裡,也不需要你手動建立或傳遞。理解它的存在,是為了建立正確的心智模型,知道暫停恢復這件事背後確實有個機制在運作。
這套自動產生 Continuation 的機制,有個正式名稱:Continuation-Passing Style,簡稱 CPS。概念上,編譯器會把一個 suspend function 改寫,讓它額外攜帶一個 Continuation 參數。函式內部原本「接下來要做的事」,會被拆解成可以透過這個 Continuation 物件在適當時機被呼叫、藉此接續執行的形式。這裡只描述轉換帶來的效果,不展開實際的位元組碼或狀態機技術細節,那些屬於編譯器實作的內部工程,不是今天要建立的心智模型。
狀態又是怎麼被保留下來的。直觀理解是,函式內部原本的區域變數與執行位置,會被編譯器安排進一個對應這個 suspend function 的狀態容器中。函式暫停後,之後可以憑藉這個狀態容器與 Continuation,準確恢復到暫停前的狀態,變數值不會遺失,執行位置也不會跑掉。這裡同樣停在讀者能建立直觀圖像的程度,不深入探討這個容器內部實際的實作標籤或組織方式。
下面這段虛擬碼只是示意用途,非真實編譯結果,目的是讓你對「多了一個 Continuation 參數」這件事有具體畫面:
// 原始寫法,開發者實際撰寫的樣子
suspend fun downloadImage(url: String): ByteArray {
val response = fetchFromNetwork(url)
return response.bytes
}
// 示意用途、非真實編譯結果
// 概念上編譯器轉換後大致的樣貌
fun downloadImage(url: String, continuation: Continuation): Any? {
// 概念上,接下來要做的事被交給 continuation 保管
// 暫停時,執行位置與 url 等狀態會被存進對應的狀態容器
// 等結果回來,協程機制會呼叫 continuation 接續執行
}
這裡的回傳型別特地寫成 Any?,多了可為 null 的彈性,用來表達「這次呼叫可能還沒有結果,要等之後才會送回來」這件事,讓呼叫端能區分這次是同步直接拿到結果,還是已經暫停、稍後才會收到結果。
拿示範情境具體化一次。想像下載圖片的 suspend function 在等待網路回應時暫停,編譯器產生的 Continuation 就代表「網路回應一旦回來之後,要接著處理這張圖片後續邏輯」這件事。等真正的回應到達時,協程機制會呼叫這個 Continuation,讓函式從暫停點接續執行,剛才存進狀態容器的 url 或其他區域變數,也會一併被正確地找回來。
理解 Continuation 存在的最大實際價值,是能夠正確解讀一段協程程式碼的執行順序。當你看到多個 suspend function 呼叫交錯在一起,知道每個暫停點背後都有一個對應的延續在等待被喚醒,能幫助你推導出程式碼實際的執行時間軸,而不是靠直覺猜測「反正它看起來是依序寫的,應該就是依序跑完」。這份時間軸的推導能力,會在系列後面處理結構化並發與取消機制時反覆用到。
還有一個值得先留印象的實務關聯。協程的例外堆疊追蹤與除錯資訊,某種程度上與這套 Continuation 機制有關。今天不展開例外處理的具體細節,那部分留給系列第三階段,這裡只建立一個印象:底層機制與日後除錯體驗之間,確實存在關聯。
今天知道了 suspend function 內部被編譯器改寫成攜帶 Continuation 的形式,具備暫停與恢復的能力。函式本身已經有了這套機制,接下來自然會冒出一個問題:這一切終究要有個起點,一段程式碼要怎麼真正發動出一個協程,讓這整套暫停恢復機制開始運作。
今天先把這個疑問留在這裡,不急著給答案。下一篇會正式定案這個問題的答案,介紹三種啟動協程的方式,詳見 《Day 04:launch、async、runBlocking,三種 Coroutine Builder 該選誰》。