C3 語言的五年設計失誤:從無號到有號的大小型別
c3-lang.org · 2026-05-02
C3 語言的設計者在五年後承認,將 usz(size type)設計為無號整數是一個錯誤,並宣布將預設大小型別改為有號整數(signed sz)。這項決定呼應了 Go 和 Java 在設計之初便選擇有號大小型別的策略。
無號大小型別的問題根源
無號整數的語意是模算術(modular arithmetic)而非「非負整數」。當程式碼在有號與無號型別之間混用時,C 和 C++ 標準規定的隱式轉換規則會靜默地產生意想不到的行為:
- 迴圈條件
i >= 0在i為無號型別時永遠成立,導致無窮迴圈 - 模除(modulo)在負值操作數上的結果在 C++ 中是實作定義行為,在無號型別下更難推理
- 環形緩衝區的邊界計算中,
head - tail在head < tail時回繞為巨大的正整數
無號型別的真正用途是:需要模算術語意(如哈希、CRC)、或需要完整的位元寬度(如 u64 旗標位元)。用它來表示「不可能為負的計數或索引」混淆了語意,反而引入錯誤。
Go 與 Java 的先例
Go 的 int 是平台原生的有號整數,其內建的 len()、cap() 均回傳有號的 int。Java 完全沒有無號整數型別(直到 Java 8 的 Integer.toUnsignedString 才部分補充)。兩個語言的設計者均有豐富的 C/C++ 背景,刻意迴避了無號大小型別帶來的轉換問題。
C3 的修正方案
新的 sz 型別為有號整數,在 64 位元平台上與 C 的 ptrdiff_t(通常為 int64_t)對應。原有的 usz 保留供需要無號語意的場景使用,但不再作為預設大小型別。API 邊界上的轉換規則也隨之簡化:大部分的陣列索引與長度運算現在都停留在有號域中,只有與 C 互動的介面才需要顯式轉換為 usz。
作者總結:「每一次變更都讓程式碼更容易推理,也更正確」——這是典型的「後見之明」設計修訂,承認了歷史慣例(C 的 size_t)對初始決策的不當影響。
可重啟序列、TCMalloc 與 Hyrum 定律:核心 API 的隱性契約
LWN.net · 2026-04-30
Linux 核心的可重啟序列(restartable sequences,rseq)是 5.1 版引入的機制,允許使用者空間執行不可被搶佔的短序列程式碼,用於無鎖的每 CPU 資料存取。TCMalloc(Google 的記憶體分配器)是 rseq 的主要使用者之一,但它對 rseq 的使用方式與核心文件的規範存在偏差。LWN 的這篇分析展示了這如何演變為核心開發者不得不支援的隱性 API 契約。
rseq 機制簡述
rseq 的運作基於在使用者空間與核心之間共享的 struct rseq。使用者空間程式設計一段「關鍵區段」:
rseq_cs.start_ip = start;
rseq_cs.post_commit_offset = end - start;
rseq_cs.abort_ip = abort_handler;若核心在關鍵區段執行期間需要搶佔或遷移 CPU,它會將 PC 跳至 abort_ip,讓使用者空間重試。這個機制的前提是關鍵區段必須極短(通常幾條指令),且不能包含系統呼叫。
TCMalloc 的非規範用法
TCMalloc 使用 rseq 來實作每 CPU 快取的無鎖存取。問題在於它在 rseq 關鍵區段結束後,沒有在規範要求的時間點清除 rseq_cs 指標。根據核心文件,這個指標應在關鍵區段結束時立即清零;TCMalloc 延遲了清零,以減少不必要的記憶體寫入。
這個行為在功能上是正確的(實際的競爭條件不存在),但違反了文件規範。當核心開發者試圖在規範基礎上增加新的優化時,便遇到了 TCMalloc 的這個假設。
Hyrum 定律的體現
Hyrum 定律(Hyrum's Law)指出:「當一個 API 有足夠多的使用者時,所有可觀察的行為——無論是否記載於文件——都會被某個使用者依賴。」TCMalloc 正是依賴了 rseq 的一個未記載行為:延遲清零指標不會引發問題。核心開發者於是面臨選擇:破壞 TCMalloc,或將這個行為納入保證。考量到 TCMalloc 在 Google 規模的生產系統中廣泛使用,實質上沒有選擇——未記載的行為成為了事實上的契約。
這個案例與 Linux 核心長期以來的「不破壞使用者空間」政策一脈相承,但也說明了為什麼核心 API 的設計者需要對「觀察到但未明文禁止」的用法保持高度謹慎。
PEP 661:Python 哨兵值標準歷時五年終獲接受
python.org · 2026-05-01
PEP 661 在提出五年後正式獲得 Python Steering Council 接受,將在 Python 3.15 中引入內建的 sentinel() 工廠函式,為哨兵值(sentinel values)提供標準化的建立方式。
哨兵值的現有問題
哨兵值是程式碼中常見的佔位模式,用於區分「未傳入值」與「傳入 None」:
_MISSING = object()
def f(x=_MISSING):
if x is _MISSING:
# 未傳入值
...這個慣用法有三個缺陷:
- repr 不友善:
f.__defaults__顯示為(<object object at 0x7f...>,),在函式簽章文件中無意義 - 型別標注困難:無法為使用哨兵的函式撰寫精確的型別簽章
- copy / pickle 後失去識別性:
copy.copy(_MISSING) is _MISSING為False,導致is比較失效
PEP 661 的解法
新的 API 極為簡潔:
from builtins import sentinel
MISSING = sentinel('MISSING')建立的哨兵物件具有以下保證:
repr(MISSING)回傳'MISSING'(即傳入的名稱字串)- 可在型別表達式中使用:
int | MISSING - 透過
copy.copy()和pickle.loads(pickle.dumps(MISSING))後,is比較仍為True - 預設為 truthy(鼓勵使用
is而非布林測試) - 只接受一個位置參數(名稱字串),其餘均為關鍵字參數保留
為何花了五年
PEP 661 曾在多個替代方案之間爭論:建立 Sentinel 基礎類別、使用 metaclass、在 enum 模組新增支援、或完全作為第三方函式庫。社群調查顯示 37% 的受訪者支持「專用哨兵工廠納入 stdlib」方案,最終成為被接受的方向。實作本身在 Python 層面很簡單,但 Steering Council 需要確認語義完整性(特別是 pickle 的識別性保證)後才批准納入內建命名空間。