題幹與適用場景
你的 Kotlin 服務有一串呼叫鏈都會使用同一個 UserService、記錄器或交易上下文。團隊希望減少重複傳參,但不想把依賴藏進全域變數,也需要逐步從 context receivers 遷移到 Kotlin 2.2 的 context parameters。請說明語意、呼叫方式、歧義處理與適用邊界。目標職位是 Kotlin/JVM 工程師;本題不假設特定 DI 框架。
面試官考察點
強回答會指出 context parameter 是編譯期可見的隱式依賴,不是執行期容器。解析發生在呼叫點,按型別尋找目前作用域的值;同層存在多個相容值會產生歧義。也要區分 context receiver 與 context parameter 的命名和遷移差異,承認此特性仍有限制,並說明普通參數更適合公開 API 或高可讀性邊界。
回答前需要釐清的問題
- 依賴是否跨越公共模組或函式庫 API?若是,顯式參數通常較容易發現、測試與跨語言呼叫。
- 依賴是否在一段區域 DSL 或交易操作中穩定不變?若是,context parameter 可以減少樣板傳遞。
- 同一作用域是否可能有兩個同型別實作?若是,必須設計命名、顯式傳入或包裝型別消除歧義。
- 專案是否已使用 context receivers?若是,先確認編譯器選項與遷移範圍,不能把兩者混在同一模組。
30 秒回答框架
「Context parameter 把依賴宣告在函式簽名上,但由呼叫點作用域提供。編譯器按型別解析;同層有多個匹配值就報歧義。它適合區域 DSL、交易或記錄上下文,能減少重複傳參;公開 API、跨語言邊界或依賴經常變動時,我仍用顯式參數。遷移時啟用對應編譯器選項,檢查舊 receiver 的呼叫與類別層級場景,因為兩者並非一對一。」
分步驟深入解答
先宣告依賴,再在作用域提供它:
interface UserService {
fun findUser(id: Int): String
}
context(users: UserService)
fun greeting(id: Int): String = "Hello ${users.findUser(id)}"
fun main() {
val service = object : UserService {
override fun findUser(id: Int) = "user-$id"
}
context(service) {
println(greeting(7))
}
}函式簽名仍宣告依賴,因此呼叫者看得到;呼叫點的 context(service) 負責提供值。解析按型別比對目前作用域,不按變數名稱優先。若同一層同時有 serviceA 與 serviceB,呼叫會失敗,而不是靜默選一個實作。可以在呼叫處顯式傳入命名 context argument,或用不同包裝型別表達不同語意。
它與普通參數的邊界取決於資訊密度。區域 DSL 中十幾個函式都需要同一個不可變上下文時,context parameter 能避免每層重複傳遞;單一函式只用一次依賴時,普通參數更直接。公開函式庫 API 還要考慮 Java 呼叫、IDE 可發現性與文件產生,隱式來源越遠,維護成本越高。
context receivers 的遷移不能只做關鍵字替換。JetBrains 的遷移說明指出,context parameters 需要命名,類別上的 context receiver 沒有直接對應物;同一模組不能同時啟用兩種模式。專案應先按模組切換編譯器參數,再逐個檢查呼叫點、可呼叫參考與類別層級 receiver。官方文件也列出限制:建構子不能宣告 context parameters,帶 context 的屬性不能有 backing field 或 initializer。
可以用匿名參數減少未使用變數,但它仍參與解析:
context(_: UserService)
fun logGreeting(id: Int) {
println(greeting(id))
}選擇規則是「區域且穩定的共享上下文用 context;跨邊界、需要顯式替換或可能多實作的依賴用普通參數或顯式 DI」。不要把 context parameter 當成全域服務定位器;依賴仍需在測試中由呼叫作用域提供,生命週期也仍由外層物件管理。
高品質示範回答
Context parameter 是編譯期的隱式依賴宣告。函式會把 UserService 寫進簽名,但呼叫者在 context(service) 作用域中提供實例;編譯器按型別從目前作用域尋找,同層有兩個匹配值就報歧義。我會在區域 DSL、交易或記錄鏈路使用它,因為這些函式共享穩定上下文;對公開 API、Java 互通或需要頻繁替換的依賴,繼續使用顯式參數或 DI。遷移 context receivers 時,我會先切換模組編譯選項,再檢查命名、可呼叫參考與類別層級 receiver,並接受建構子和帶 backing field 屬性等限制。
常見錯誤
- 錯誤表現 → 說 context parameter 是執行期依賴注入容器 → 失敗原因是解析由編譯器在呼叫點完成 → 修正方法是解釋作用域、型別匹配與編譯期歧義。
- 錯誤表現 → 認為同型別實例會按變數名稱自動選擇 → 失敗原因是同層多匹配值會直接報歧義 → 修正方法是顯式 context argument 或使用語意包裝型別。
- 錯誤表現 → 把 context receiver 直接改名為 context parameter → 失敗原因是命名要求、類別層級場景與可呼叫參考有差異 → 修正方法是按模組遷移並逐個檢查呼叫點。
- 錯誤表現 → 所有依賴都改成隱式 → 失敗原因是公開邊界的依賴可發現性下降 → 修正方法是把 context 限定在區域穩定上下文,邊界使用普通參數。
追問及應對
追問一:測試函式時如何替換 context 依賴?
在測試建立 fake 或 stub,實作同一介面,再用 context(fake) { ... } 執行被測函式。若測試需要頻繁切換多個實作,顯式參數通常更清楚,也更容易在測試報告看見依賴。
追問二:兩個同型別服務都必須存在怎麼辦?
為介面增加語意包裝型別,例如 PrimaryUserService 與 AuditUserService,或在呼叫點使用顯式 context argument。不要依賴宣告順序,因為解析規則不把順序當成優先級。
追問三:為什麼不能把 context parameter 放進建構子?
目前 Kotlin 文件明確限制建構子不能宣告 context parameters。需要物件層級依賴時,使用普通建構參數或顯式工廠;強行改成全域上下文會隱藏生命週期與執行緒邊界。