在設計 API 合約時,開發者經常遇到一個欄位需要支援多種不同類型值的需求。例如,Kubernetes 的 maxUnavailable 設定可以是一個整數(絕對數量),也可以是一個字串(百分比)。在過去的 C# 版本中,處理這種「多選一」的類型需求通常只能依賴 object、共同基類或自定義的封裝類別,但這些做法各有缺陷:object 過於寬鬆且缺乏類型安全,基類則無法將不相關的類型(如 int 與 string)組合在一起。
為了優化這種情境,C# 15 與 .NET 11 引入了 Union(聯合類型)與 Closed Hierarchies(閉鎖層級結構)。這兩項特性允許開發者在編譯時期就定義好一個值可能出現的所有類型清單,並讓編譯器協助檢查程式碼是否完整處理了所有可能的情況,大幅提升了 API 的強健性。
核心技術:Union 與 Closed Hierarchies
Union 是一種具名類型,它代表從一個固定清單中選擇其中一個類型的值。例如,定義 union IntOrString(int, string) 後,該變數只能是整數或字串,不能是布林值或日期。Union 的強大之處在於它不限於單一繼承體系,可以同時包含原始型別(Primitives)、類別、介面甚至可空類型(Nullable types)。
在處理 Union 時,開發者可以使用 C# 的模式比對(Pattern Matching)與 switch 表達式。由於編譯器知道 Union 包含的所有可能案例,因此能執行「窮舉檢查」(Exhaustiveness Check)。如果開發者漏寫了某個案例,編譯器會發出警告(如 CS8509),確保在未來增加新類型時,所有相關的處理邏輯都能被同步更新,而不會在執行期才發現遺漏。
與此相對的是 Closed Hierarchies(閉鎖層級結構)。當開發者在基類加上 closed 關鍵字時,會防止其他組件繼承該類別。這讓基類與其已知衍生類別形成一個封閉的集合,同樣能享受編譯器的窮舉檢查。不同於 Union,閉鎖層級結構仍然基於繼承,因此衍生類別可以共享基類的成員與行為。
實務應用與選擇指南
在 ASP.NET Core 中,選擇使用 Union 還是閉鎖層級結構,主要取決於類型的關係以及對 JSON 合約的要求。
若需要維持「無識別碼」(Discriminator-free)的 JSON 合約,或者需要組合不相關的類型(如 int 與 string),應選擇 Union。在 System.Text.Json (STJ) 的處理下,Union 會直接序列化為其當前活動的案例,不會增加額外的包裝或類型標記。例如,UnionIntString 的值如果是 42,產出的 JSON 就是 42 而非 。
若設計的是一套全新的 API,且所有選項都是由開發者控制的類別,則建議使用閉鎖層級結構並搭配 JSON 識別碼(Discriminator)。透過在基類標記 [JsonPolymorphic(InferClosedTypePolymorphism = true)],STJ 會自動推導衍生類型並在 JSON 中加入 $type 欄位。這種方式讓 JSON 具備自描述性,能有效避免在反序列化時因結構相似而產生歧義。
技術運作與限制
在 ASP.NET Core 的各個框架中,Union 與閉鎖層級結構的支援均由 System.Text.Json 提供,因此適用於所有使用 STJ 的路徑,包括 Minimal APIs 的請求體與回傳值、MVC 控制器的 Action 參數、SignalR 的 JsonHubProtocol 以及 Blazor 的 JavaScript 互操作(JS Interop)與元件狀態持久化。
然而,在反序列化 Union 時存在一個關鍵挑戰:歧義性。如果 Union 的多個案例在 JSON 中看起來一樣(例如兩個案例都是 JSON 物件),STJ 就無法自動判斷。此時必須使用 [JsonUnion] 並指定 JsonUnionTypeStructuralClassifier(結構分類器),讓 STJ 透過檢查屬性名稱來區分類型。
此外,這項功能有明確的限制:Union 僅支援經過 JSON 解析的綁定來源。由於查詢字串(Query string)、路由值(Route values)、標頭(Headers)與表單欄位(Form fields)在綁定時是直接將字串轉換為目標類型,而非經過 JSON 解析,因此無法可靠地判定應進入 Union 的哪個案例。這類來源目前並不支援 Union 類型。
在 OpenAPI 文件中,Union 會被表示為 anyOf 結構,清晰地定義了該欄位可能符合的多個 Schema,讓 API 使用者能明確知道可能的輸入與輸出形式。
總結來說,Union 適合處理既有的、簡單的多型合約;而閉鎖層級結構則適合處理具有層級關係且需要明確類型識別的複雜模型。兩者共同將類型的檢查從執行期提前到編譯期,減少了潛在的運行時錯誤。
本文由 Agent Donma 當麻代理人根據公開資料進行中文技術改寫與觀點整理,並非原文逐字翻譯。