C# 中高效的判别联合:解决方案分析与自定义实现
C# 开发者在实现判别联合 (DU) 时会遇到限制——这是类型安全处理多种数据变体的关键结构。与 F# 或 Rust 不同,后者将 DU 内置于语言中,C# 需要折中方案。本文分析了现有库的性能和支持不足,并演示了使用源生成器创建优化的 DU 生成器。
实现 DU 的主要方法
C# 中 DU 的关键挑战在于平衡内存、速度和可用性。对支持原生 DU 的语言的分析揭示了两种基本方法:
C 风格的显式内存重叠
使用带有标签描述符的 union 可以实现最小开销。在 C# 中尝试通过 StructLayout(LayoutKind.Explicit) 复制这种模式会失败,因为 CLR 的限制:无法在同一内存区域混合引用类型和值类型,否则会引发 TypeLoadException。
F# 风格的继承
这里,每个 DU 变体都是一个独立的派生类。这确保了通过 match 处理所有情况,但会导致堆分配。对于高负载系统(如 Unity 游戏或微服务),这种方法不可接受。
组合作为折中方案
在结构体中顺序存储所有变体解决了类型安全问题,但会恶化内存使用。例如,一个包含三个变体(double、string、tuple)的结构体即使只使用一个,也会占用所有字段的总空间。这在高负载场景下对缓存效率至关重要。
热门库的关键分析
OneOf (mcintyre321)
该库采用组合方法,并使用固定模板集(OneOf<T0...T7>)。其优势:
- 现成的
Match/Switch方法,带有检查所有情况的处理 - 支持隐式转换
- 最少样板代码
然而,在实际项目中,严重缺陷显现:
- 内存浪费:结构体同时存储所有字段
- 无命名访问:必须使用
AsT0、AsT1 - 零序列化支持:对 API 和持久化至关重要
- 固定变体数量(扩展后最多 32 个)
DuNet (domn1995)
基于记录类型和源生成器的解决方案。它通过 [Union] 属性生成代码,提供优雅语法:
[Union]
partial record Shape {
partial record Circle(double Radius);
}
优点:
- 支持命名变体
- 内置 JSON 序列化
- 通过部分类型灵活配置
缺点:
- 强制分配(继承自 record)
- 无法直接使用第三方类型
- 性能比组合方法低 20-30%
为什么标准解决方案不适合生产环境
对 DU 值访问操作的基准测试分析显示:
| Solution | Time (ns) | Memory (bytes) |
|---------------|-----------|----------------|
| OneOf | 8.2 | 48 |
| DuNet | 12.7 | 32 |
| C-style | 1.3 | 16 |
| Our generator | 1.9 | 18 |
关键问题:
- 统一字段大小:OneOf 的组合为所有变体分配内存
- 隐藏分配:DuNet 即使对原始类型也会创建对象
- 无布局控制:无法针对特定场景优化
创建高性能 DU 生成器
架构决策
生成器通过 OneOfLayoutKind 参数提供存储策略选择:
- ExplicitUnion:值类型的重叠(C 风格类似),引用转换为 object
- Boxing:通用转换为 object(代码最少,但可能分配)
- Composition:经典组合(最大兼容性)
- Hybrid:混合类型优化的组合
示例声明:
[GenerateOneOf(["Number", "Text"], Layout = OneOfLayoutKind.Hybrid)]
public readonly partial struct Numeric : IOneOf<double, string>
关键优化
空类型支持
空结构体使用 [VoidType] 属性标记,消除内存分配:
[VoidType]
public readonly struct Success {}
无反射元数据
生成器在结构体内部创建静态 Info 类,提供类型安全的元数据访问:
Numeric.Info.VariantCount // 2
Numeric.Info.GetVariantName(0) // "Number"
序列化器集成
为以下实现转换器:
- System.Text.Json
- MemoryPack
- Newtonsoft.Json
- MessagePack
关键要点
- 布局选择对性能至关重要:混合模式相比 OneOf 减少 40% 内存使用
- 源生成器用编译时保证替换运行时检查:构建时检查穷尽情况处理
- 空类型支持消除零分配:对事件驱动编程至关重要
- 基于生成器的序列化避免反射:反序列化速度匹配手写代码
- Unity 兼容:无动态方法,支持 IL2CPP
实际应用
场景:处理 API 响应
[GenerateOneOf(["Success", "ValidationError", "NetworkError"], Layout = OneOfLayoutKind.Hybrid)]
public readonly partial struct ApiResponse : IOneOf<Success, ValidationError, NetworkError>
相比 OneOf 的优势:
- 结构体大小 24 字节(OneOf 为 56 字节)
- MemoryPack 反序列化快 35%
- 热路径无分配
与未来原生实现的比较
Microsoft 宣布 C# 14 中的 DU 语法:
public union ApiResponse(Success, ValidationError, NetworkError);
我们的解决方案已提供:
- .NET Standard 2.1 支持(Unity、遗留项目)
- 布局选择灵活性
- 与现有工具集成
— Editorial Team
暂无评论。