返回首页

C# 中的判别联合:生产环境的分析与优化

C# 中实现判别联合的现有解决方案分析。OneOf 和 DuNet 的性能比较,创建支持序列化和 Unity 的优化源生成器。高负载系统的实用推荐。

C# 中的高效判别联合:如何绕过语言限制
Advertisement 728x90

C# 中高效的判别联合:解决方案分析与自定义实现

C# 开发者在实现判别联合 (DU) 时会遇到限制——这是类型安全处理多种数据变体的关键结构。与 F# 或 Rust 不同,后者将 DU 内置于语言中,C# 需要折中方案。本文分析了现有库的性能和支持不足,并演示了使用源生成器创建优化的 DU 生成器。

实现 DU 的主要方法

C# 中 DU 的关键挑战在于平衡内存、速度和可用性。对支持原生 DU 的语言的分析揭示了两种基本方法:

C 风格的显式内存重叠

Google AdInline article slot

使用带有标签描述符的 union 可以实现最小开销。在 C# 中尝试通过 StructLayout(LayoutKind.Explicit) 复制这种模式会失败,因为 CLR 的限制:无法在同一内存区域混合引用类型和值类型,否则会引发 TypeLoadException

F# 风格的继承

这里,每个 DU 变体都是一个独立的派生类。这确保了通过 match 处理所有情况,但会导致堆分配。对于高负载系统(如 Unity 游戏或微服务),这种方法不可接受。

Google AdInline article slot

组合作为折中方案

在结构体中顺序存储所有变体解决了类型安全问题,但会恶化内存使用。例如,一个包含三个变体(double、string、tuple)的结构体即使只使用一个,也会占用所有字段的总空间。这在高负载场景下对缓存效率至关重要。

热门库的关键分析

OneOf (mcintyre321)

该库采用组合方法,并使用固定模板集(OneOf<T0...T7>)。其优势:

Google AdInline article slot
  • 现成的 Match/Switch 方法,带有检查所有情况的处理
  • 支持隐式转换
  • 最少样板代码

然而,在实际项目中,严重缺陷显现:

  • 内存浪费:结构体同时存储所有字段
  • 无命名访问:必须使用 AsT0AsT1
  • 零序列化支持:对 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

Advertisement 728x90

继续阅读