c# 15 的 union 关键字才是原生可辨识联合类型,oneof 是第三方泛型包装库,无编译期穷尽检查、无值语义、不参与模式匹配验证,仅适用于旧项目过渡。

C# 15 的 union 关键字才是原生可辨识联合类型,OneOf 是第三方库(如 OneOf NuGet 包),两者语法、语义、编译期保障完全不同——别混用,也别指望 OneOf 能触发 C# 15 的穷尽检查。
为什么 OneOf<t1 t2></t1> 不是真正的可辨识联合类型
OneOf 库本质是泛型包装器 + 运行时标记(Index 属性),它没有编译器级的类型联合定义,也不参与 C# 15 的模式匹配穷尽性分析。你写 match 表达式时,编译器不会报错说“漏了 OneOf<int string bool></int> 中的 bool 分支”——它只当你是普通对象在做 switch 或 if 判断。
-
OneOf的Switch/Match方法是库提供的扩展方法,非语言特性,无法静态验证覆盖所有泛型参数类型 - 它的
Value属性返回object,丢失类型信息,需手动as或is检查,空安全弱 - 内存布局是引用类型堆分配(即使包装值类型),有 GC 开销;而 C# 15
union是栈安全、零装箱的值语义 - 不支持嵌套联合定义(如
union Result = Success of int | Failure of Error),OneOf只能扁平展开
union 声明后必须用 switch 或 is 模式匹配处理所有分支
C# 15 编译器会强制你在使用联合类型变量时,覆盖其所有事例(case)。这不是建议,是编译错误。
- 写
if (x is int i)但没写else if (x is string s)?编译失败:CS8512 “未处理联合类型中的事例string” - 用
switch时漏掉某个事例,或没写default(且联合类型不可为空),同样报错 - 若联合含
null,如public type Response = Success | Error | null,则switch必须显式处理null分支,不能靠default隐式兜底 - IDE(VS 2026 P3 / Rider 2026.1)会实时高亮缺失分支,并提供快速修复插入空分支存根
union 类型不能继承、不能实现接口、不能作为泛型约束
这是有意设计限制:联合类型是封闭的、终态的类型集合,不是可扩展的抽象基类。
- 不允许
public type MyUnion = int | string : IConvertible—— 接口实现必须由每个事例单独声明(如record Success(int Code) : IConvertible) - 不能写
where T : union,泛型约束只接受 class/interface/struct 等传统分类 - 不能从
union派生新类型,也不能把union当作基类赋值给object外的其他类型变量(除非显式转换) - 序列化需注意:System.Text.Json 默认不识别
union,需注册自定义JsonConverter<myunion></myunion>,否则抛NotSupportedException
工业场景中误用 OneOf 替代 union 的典型后果
在 PLC 响应解析等强状态一致性要求的场景,用 OneOf<plcdata plctimeout plcerror></plcdata> 看似可行,但实际埋雷:
- 新增一种响应类型(如
PlcBusy)后,所有Match调用点不会编译报错,运行时才因未覆盖分支抛InvalidOperationException -
OneOf的IsT0/IsT1属性是运行时反射查表,性能比union的内联switch低 3–5 倍(实测 100 万次循环) - 与 OPC UA 客户端 SDK 返回的
union类型混用时,无法直接赋值或比较,必须手动解包再构造,极易出错 - 团队新人看到
OneOf示例代码,会误以为这就是 C# 标准方案,后续在 .NET 11 项目中继续沿用,错过原生联合类型的类型安全红利
真正该做的,是把 OneOf 当作过渡方案——仅用于尚不支持 C# 15 的旧项目,新项目一律用 union 声明 + 编译器兜底。最易被忽略的一点:联合类型的字段名、事例名一旦发布,就和 API 兼容性绑定,改名=破坏性变更,比改类名影响更广。











