表示「有限封閉值域」的領域概念(國家代碼、訂單狀態、貨幣類型)是領域建模核心。C# 內建 enum 提供編譯期型別安全,但受限於底層必為整數、無法攜帶行為/屬性、無法本地化顯示。Java 社群推廣的 Type-safe Enum Class (枚舉類) 模式以類別封裝常量實例,突破了這些限制。本文深度對比兩種模式在 C# 中的實現、序列化陷阱、操作符重載解法、擴展能力、適用場景,並給出決策指南。
public enum Sex { Male, Female }
public enum CountryCode : short { US = 1, CN = 86, JP = 81 } // 指定底層型別
| 優勢 | 限制 |
|---|---|
| 值類型,零堆配置,效能極佳 | 底層必為整數 (sbyte/byte/short/ushort/int/uint/long/ulong) |
| 編譯期型別安全,switch 完整性檢查 | 無法附加屬性/方法 (顯示名、代碼、排序權重...) |
| 語法簡潔,IL 直接支援 | 無法本地化顯示 (ToString 只能輸出名稱) |
| FlagsAttribute 支援位組合 | 無法分組/分層 (如亞洲國家子集) |
| 序列化原生支援,無驚喜 | 無法實現型別轉換邏輯 (Parse/TryParse 需外部) |
[Serializable]
public sealed class Sex {
// 私有建構函數:確保值域封閉
private Sex(string name, string displayName) {
Name = name;
DisplayName = displayName;
}
// 唯一靜態實例
public static readonly Sex Male = new Sex("Male", "男");
public static readonly Sex Female = new Sex("Female", "女");
// 屬性
public string Name { get; }
public string DisplayName { get; }
// 序列化安全:重載 ==/!= + 覆寫 Equals/GetHashCode
public static bool operator ==(Sex left, Sex right) {
if (ReferenceEquals(left, null)) return ReferenceEquals(right, null);
return left.Equals(right);
}
public static bool operator !=(Sex left, Sex right) => !(left == right);
public override bool Equals(object obj) {
return obj is Sex other && Name == other.Name;
}
public override int GetHashCode() => Name.GetHashCode();
// 友好顯示
public override string ToString() => DisplayName; // 或依 CultureInfo 本地化
// 型別轉換
public static Sex Parse(string name) {
return name switch {
"Male" => Male,
"Female" => Female,
_ => throw new ArgumentException($"Unknown sex: {name}")
};
}
public string ToDbValue() => Name; // 持久化值
}
var formatter = new BinaryFormatter();
var stream = new MemoryStream();
formatter.Serialize(stream, Sex.Male);
stream.Position = 0;
Sex deserialized = (Sex)formatter.Deserialize(stream);
Console.WriteLine(deserialized == Sex.Male); // false! 反序列化產生新實例
readResolve,C# 無此機制如上代碼:operator == 委派給 Equals,Equals 比較 Name (不變識別鍵)。要求:
Equals/GetHashCode 保持一致==/!= 供 C# 使用者直觀比較[Serializable]
public sealed class Country {
private Country(string code, string name, string region, string lang) {
Code = code; Name = name; Region = region; Lang = lang;
}
public string Code { get; }
public string Name { get; }
public string Region { get; } // 分組屬性
public string Lang { get; } // 附加屬性
public static readonly Country US = new Country("US", "United States", "NorthAmerica", "en");
public static readonly Country CN = new Country("CN", "中國", "Asia", "zh-CN");
public static readonly Country JP = new Country("JP", "日本", "Asia", "ja");
// 分組查詢
public static IEnumerable GetByRegion(string region) =>
All.Where(c => c.Region == region);
// 靜態集合支援 LINQ
public static IEnumerable All => new[] { US, CN, JP };
// 本地化顯示
public string GetDisplayName(CultureInfo culture) =>
culture.TwoLetterISOLanguageName switch {
"zh" => Name, // 假設 Name 已是中文
_ => Name
};
// ... operator ==, Equals, Parse, ToDbValue 同 Sex
}
若需值類型語義 (堆疊分配、無 null、複製語義) 且值域非嚴格封閉 (允許使用者自訂值),可用 struct:
public readonly struct Color {
public static readonly Color Red = new Color(0xFF0000, "Red");
public static readonly Color Green = new Color(0x00FF00, "Green");
public static readonly Color Blue = new Color(0x0000FF, "Blue");
public int Rgb { get; }
public string Name { get; }
public Color(int rgb, string name) { Rgb = rgb; Name = name; }
// 允許自訂值
public static Color FromRgb(int rgb) => new Color(rgb, $"Custom#{rgb:X6}");
// 運算子重載支援值比較
public static bool operator ==(Color a, Color b) => a.Rgb == b.Rgb;
public static bool operator !=(Color a, Color b) => a.Rgb != b.Rgb;
// ... Equals/GetHashCode
}
差異:struct 必須有無參數建構函數 → 無法強制封閉值域;適合「預設值集合 + 開放擴展」場景 (如 System.Drawing.Color)。
| 場景 | 推薦 | 理由 |
|---|---|---|
| 純內部狀態機、無顯示需求、效能極敏感 | enum | 零開銷、編譯期最佳化、switch 完整性 |
| 需本地化顯示名、需附加屬性 (代碼、排序、圖示) | Enum Class | 封裝豐富資料、ToString 多態、屬性擴展 |
| 需分組/分層查詢 (如地區→國家) | Enum Class | 靜態集合 + LINQ |
| 需型別轉換邏輯 (Parse/TryParse/資料庫值) | Enum Class | 封裝轉換規則、單一責任 |
| 跨進程/分散式序列化傳遞 | Enum Class (+ operator ==) | 值相等語義存活反序列化 |
| 位旗標組合 (Read/Write/Execute) | enum + [Flags] | 原生位運算、IDE 支援 |
| 預設值集 + 開放自訂 (顏色、字體大小) | readonly struct | 值語義 + 擴展性 |
定義來源資料 (JSON/YAML/CSV) → Source Generator 產生 partial class Country { ... } → 開發者補充業務邏輯。實現「資料驅動枚舉」,避免手寫樣板代碼。
enum 是輕量級值類型枚舉,適合單純狀態表示;Enum Class 是功能豐富的參考類型枚舉,適合領域模型中攜帶行為、屬性、本地化、轉換邏輯的核心概念。關鍵技術點:私有建構函數封閉值域、靜態 readonly 實例、操作符重載解決序列化身分識別、覆寫 Equals/GetHashCode/ToString、Parse/ToDbValue 封裝轉換。readonly struct 提供第三種選擇:值語義 + 開放擴展。根據領域複雜度、序列化需求、效能預算、團隊慣例選擇最適模式。