iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Software Development

AI時代下的軟體工程系列 第 9 篇

Day9: Type Checker:檢查是否有矛盾的地方

  • 分享至 

  • xImage
  •  

What is Type Checker

Type checker,中文是型別檢查器。型別在程式裡描述的是值的定義,以及可以對它做哪些操作:例如:int 可以做整數運算,dict 可以用 .get() 取值,None 則表示沒有值,當然也就沒有 .get() 可以呼叫。

型別檢查器會讀取程式碼,根據型別標註、運算與控制流程,檢查值的使用方式是否與型別相容,不必先執行程式。
例如型別為 int 的值,就沒辦法使用 .get()。

看一個例子:假設設定資料可能是一個字典,也可能是 None。

def get_timeout(config: dict[str, int] | None) -> int:
    return config.get("timeout", 30)

dict[str, int] 表示字典的 key 是字串、value 是整數;| None 表示也可能沒有設定資料;-> int 則宣告回傳值應是整數。

上面的程式拿到符合標註的字典時可以正常運作,拿到 None 就會出錯。Pyright 不必等到那次執行,就能從標註發現 config 可能沒有 .get(),並以 reportOptionalMemberAccess 回報問題。

矛盾就在這裡:參數允許 None,函式裡卻把它當成一定是字典。 我們可以把 type checker 想成一台「型別上的矛盾偵測器」,檢查宣告、推論出的型別與實際用法是否相容。

要補充的是,這些標註提供的是可供工具檢查的介面要求,但 Python 不會自動依照標註驗證傳入值。來自檔案、網路或使用者輸入的資料,仍需要在執行時驗證。Python 官方文件也明確區分了型別標註與執行期的行為。

Why we need type checker

沈默的程式碼,可能只是還沒走到出錯的那條路。

沒有編譯錯誤、執行時沒有崩潰,甚至沒有留下任何日誌(logs)——這些沈默,可能代表程式運作正常,也可能是觸發錯誤的條件尚未出現,或程式早已算錯,卻沒有任何檢查與紀錄。沒有錯誤訊號,不代表沒有問題。

型別檢查能讓其中一類問題提早出聲:不必等到那台機器上真的少了設定檔,就先指出型別與用法不相容,把錯誤提前到開發時處理。

那工具又怎麼知道你修好了?也處理 config 為 None 的情況即可:

def get_timeout(config: dict[str, int] | None) -> int:
    if config is None:
        return 30
    return config.get("timeout", 30)

這樣進入函式時 config 有兩種可能:字典或 None。None 的分支已經提早回傳,因此能走到最後一行的,只剩字典。工具根據控制流程縮小型別範圍,這叫型別縮窄(type narrowing)——它不需要知道這次實際傳入什麼,就能判斷最後一行的用法是否安全。

它跟前兩個工具的差別

把系列到目前為止的三個工具排在一起,差別會比較清楚:

工具 拿什麼跟什麼比 需要執行程式嗎
formatter 程式碼 vs 排版規則 否
linter 程式碼 vs lint 規則 否
type checker 值的型別 vs 使用方式 否

前兩列右邊那份標準是從外面帶進來的:一個由工具決定,一個是前人踩過坑之後寫下來的。type checker 比對的則是你自己寫下的型別,與你自己寫下的用法——這也讓它成為這條線上第一個要先寫點東西、工具才動得了的。

型別檢查怎麼進到專案裡

各種語言的代表工具

Python、JavaScript 這類動態型別語言,可以額外加入靜態型別檢查;C、C++、Go、Rust 這類靜態型別語言,則已經把這項工作整合進編譯流程。靜態型別語言也有 type checker,只是它常常是編譯器的一部分。

語言 代表工具 使用例子
Python Pyright pyright:檢查專案
JavaScript TypeScript 編譯器 tsc tsc --allowJs --checkJs --noEmit index.js:檢查 JS 檔案
TypeScript tsc tsc --noEmit:檢查型別,不輸出 JS
C Clang 編譯器 clang -fsyntax-only main.c:檢查語法與型別,不產生執行檔
C++ Clang 編譯器 clang++ -fsyntax-only main.cpp:檢查語法與型別,不產生執行檔
Go Go 編譯器 go build ./...:建置時包含型別檢查
Rust rustc,透過 Cargo 呼叫 cargo check:檢查專案,不產生最終執行檔

同一個動作,差別在誰做、以及可不可以不做。動態型別語言通常可以選擇不裝;靜態型別語言沒有這個選項,過不了就編不出來。

以 Pyright 為例:從檢查錯誤到補足資訊

選好工具之後,還要決定檢查到什麼程度。以 Python 為例,沒有標註時,Pyright 仍能從 count = 3 推論出整數型別;但當標註與推論都無法提供足夠資訊,就會出現檢查盲點。Pyright 以 Unknown 表示這類未知的型別。

所以,工具沒有報錯,也可能只是資訊不足。 能不能要求開發者補足資訊?這正是嚴格模式處理的一部分問題。

Pyright 提供四種模式。以下摘錄官方設定表的預設差異;「—」代表該項不回報,「錯誤」代表以 error 回報。

檢查項目(規則) off basic standard strict
傳入參數型別不符(reportArgumentType) — 錯誤 錯誤 錯誤
對可能為 None 的值取用屬性(reportOptionalMemberAccess) — 錯誤 錯誤 錯誤
覆寫方法的型別不相容(reportIncompatibleMethodOverride) — — 錯誤 錯誤
參數缺少型別標註(reportMissingParameterType) — — — 錯誤
變數型別未知(reportUnknownVariableType) — — — 錯誤

off 仍可能回報未定義變數等問題,不代表完全不檢查。往右走有兩件事在同時發生:一是開啟更多型別規則(例如 reportIncompatibleMethodOverride),二是降低對「我根本不知道這是什麼型別」的容忍度——strict 會主動指出缺少標註或型別未知的地方,但它並不要求每個區域變數都手動標註。

既有專案不必一次到位。Pyright 支援依路徑啟用嚴格檢查,可以先用 basic 擋住最明顯的問題,再把核心目錄轉成 strict,之後一個目錄一個目錄往上轉:

[tool.pyright]
typeCheckingMode = "basic"
strict = ["src/core"]

Type checker 如何幫助 AI

Coding agent 修改程式時,可能只讀了部分檔案。假設它為了處理檔案不存在的情況,把 load_config 改成可能回傳 None,卻漏掉其他檔案裡仍直接呼叫 .get() 的呼叫端。被修改的函式本身完全合理,問題出在介面改了、呼叫端沒跟上。

這類錯誤的共同點不是程式碼長得可疑,而是兩個地方對同一個介面有不同假設。而型別檢查器能分析納入檢查範圍的檔案,指出這類跨檔案問題,不需要 agent 先把每個呼叫端都放進當下的對話裡。

型別資訊與檢查結果,分別提供兩種幫助:

  • 寫之前,提供介面資訊。 參數接受什麼、回傳值可能是什麼,都直接附在程式碼上,讓 agent 有明確依據。
  • 寫之後,提供修正線索。 錯誤訊息指出位置、預期型別與不相容之處,讓 agent 知道哪個假設需要調整。

於是 Day8 的檢查迴圈可以繼續延伸:

讀取型別資訊 → 修改程式 → 執行型別檢查 → 依錯誤訊息修正

但標註本身不保證 agent 會遵守,必須真的執行檢查。而「讓錯誤消失」也不一定等於修好問題:Any 允許略過許多靜態型別限制,把原本明確的型別改成 Any,或加入忽略檢查的註解,都可能只是讓工具看不見矛盾。Review 時仍要確認,它修正的是程式與介面,還是放寬了檢查。


型別檢查能檢查回傳值是否符合 int,卻無法只靠 -> int 判斷 timeout 的預設值應該是 30 還是 60。一個算錯的數字,依然可能符合型別。

下一步,需要把預期行為寫成具體案例,執行程式來核對結果。明天來談 test。


上一篇
Day8: Linter:程式能跑,不代表它沒問題
下一篇
Day10: Test:把預期行為寫成可執行的例子
系列文
AI時代下的軟體工程 共 12 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言