我如何用表达式树打造零开销的枚举转换器
一、问题的起源
在我们的工业自动化系统中,存在这样一个场景:
视觉识别DLL 和 上位机软件 各自定义了功能相似的枚举,但命名和数值都不一致。在系统架构调整之前,代码中充斥着这样的转换逻辑:
csharp
// 到处可见的硬编码转换
if (visionResult == VisionStatus.Success)
{
plcStatus = PlcCommand.Execute;
}
这不仅让代码臃肿,更埋下了维护的隐患。
二、我的目标:一个简洁的API
我希望框架的使用者能这样调用:
csharp
// 一行代码完成转换,无需任何初始化
var target = EnforceParse<SourceEnum, TargetEnum>.Convert(sourceValue);
核心理念:API必须简洁,所有复杂性都封装在框架内部。
三、方案的演进
方案1:受Stateless库启发的最初实现
csharp
internal static class EnumUtils<TSource, TTarget>
where TSource : struct, Enum
where TTarget : struct, Enum
{
public static TTarget Converter(TSource source)
{
object value = Convert.ChangeType(source, Enum.GetUnderlyingType(typeof(TSource)));
try
{
return (TTarget)Enum.ToObject(typeof(TTarget), value);
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Cannot convert enum value '{source}' to {typeof(TTarget).Name}", ex);
}
}
}
测试结果:9毫秒/次
这个性能在工业控制场景完全不可接受。PLC的扫描周期通常是20-50毫秒,一次转换就占用近一半的时间。
方案2:引入字典缓存
csharp
public static Dictionary<TSource, TTarget> EnumConvert = new();
public static TTarget Resolve(TSource source)
{
EnumConvert.TryGetValue(source, out var target);
return target;
}
字典查找的时间复杂度是O(1),后续调用几乎零开销。
问题:字典需要预热。这意味着框架的使用者必须在程序启动时手动初始化,这违背了API简洁性的设计原则。
方案3:表达式树 — 最终答案
csharp
public static class EnforceParse<TSource, TTarget>
where TSource : struct, Enum
where TTarget : struct, Enum
{
public static readonly Func<TSource, TTarget> Convert;
static EnforceParse()
{
var sourceParam = Expression.Parameter(typeof(TSource), "source");
// 将源枚举转换为底层类型(如 int)
var toUnderlying = Expression.Convert(sourceParam,
Enum.GetUnderlyingType(typeof(TSource)));
// 再转换为目标枚举类型
var toTarget = Expression.Convert(toUnderlying, typeof(TTarget));
// 编译成原生委托
var lambda = Expression.Lambda<Func<TSource, TTarget>>(toTarget, sourceParam);
Convert = lambda.Compile();
}
}
四、性能实测
测试代码:
csharp
var sw = Stopwatch.StartNew();
var s1 = EnforceParse<TestLowercase, TestUppercase>.Convert(TestLowercase.e);
Logger.Info($"首次调用:{sw.ElapsedTicks} ticks");
sw.Restart();
var s2 = EnforceParse<TestLowercase, TestUppercase>.Convert(TestLowercase.e);
Logger.Info($"后续调用:{sw.ElapsedTicks} ticks");
实测结果:
text
2026-06-13 10:57:37.982 [1] INFO TangdaoFrameworkDemo.AppStartup
初次调用:54537,结果E
2026-06-13 10:57:37.982 [1] INFO TangdaoFrameworkDemo.AppStartup
后续调用:2,结果E
性能提升:从 9 毫秒 到 0.0003 毫秒,提升了 3,0,000 倍!
五、为什么这么快?
很多人误以为表达式树是反射的一种。这是一个常见的认知误区。
反射:每次调用都要解析元数据,产生装箱拆箱开销。
il
// 反射方案的IL:6+条指令 + 方法调用 + 装箱
IL_0000: ldarg.0
IL_0001: call Convert.ChangeType
IL_0006: box
...
表达式树:只在首次编译时有开销,之后就是纯原生代码。
il
// 表达式树编译后的IL:3条指令,无方法调用
IL_0000: ldarg.0
IL_0001: conv.i4
IL_0002: ret
这就是零开销抽象的威力。
六、技术要点总结
| 关键技术 | 作用 |
|---|---|
| 泛型静态构造器 | 类型首次使用时自动初始化,对使用者透明 |
| 表达式树 | 运行时生成高效的IL代码 |
| Func委托缓存 | 后续调用直接执行原生代码 |
| readonly字段 | 确保委托只编译一次,线程安全 |
七、给框架开发者的建议
- 不要过早放弃:我的方案2(字典)被否定后,差点放弃。表达式树打开了新思路。
- 突破认知边界:表达式树不是反射,它是代码生成器。这个认知转变是关键。
- 性能目标要激进:工业场景下,"够用"往往意味着"不够用"。
- API简洁是最高优先级:使用者不该知道你的内部实现有多复杂。
八、延伸思考
这个模式可以推广到更多场景:
- 类型之间的零开销转换
- 高性能的AOP实现
- 动态生成的计算逻辑
表达式树的 Compile() 方法,是连接动态灵活性和静态性能的桥梁。

浙公网安备 33010602011771号