Csharp枚举类型的装拆箱
1 优化位枚举(20260201)
1.1 原生HasFlag问题
- 原生的HasFlag方法会将枚举装箱为Enum类型,产生性能开销
- Enum本身是一个引用类型,所有具体的枚举类型比如MyColor都是值类型,并且继承自Enum(特殊的继承规则,就像所有C#对象都继承自object,它也是引用类型)。当调用基类Enum的方法时,自身需要装箱成Enum,参数也需要传递给Enum flag,会进行两次装箱
- HasFlagFast 用泛型值类型参数替代 Enum 引用类型参数,从根源上避免了装箱。Unsafe.As 只是进一步优化了类型转换的性能,而泛型值类型参数才是避免装箱的关键
1 | // 原生判断函数是Enum的实例方法,Enum本身是抽象类(引用类型) |
1.2 内部发生了什么
1 | bool result1 = myEnum.HasFlag(TestEnum.Flag2); |
通过SharpLab查看IL如下,发现进行了两次装箱
1 | IL_0003: ldloc.0 |
- 不只是Enum,所有值类型(int/struct)调用其引用类型基类(Enum/ValueType/object)的实例方法,(大部分)都会触发装箱。
- 因为值类型存储在栈上,没有方法表;引用类型存储在堆上,有方法表
- 要调用引用类型基类的实例方法,必须先把值类型 “包装” 成堆上的引用类型对象(装箱),才能通过方法表找到对应的方法。
- 但是有例外:如果值类型重写了基类方法,就不会触发装箱(比如 int 重写了 Equals/ToString),调用该方法时不会装箱 —— 因为方法已经属于值类型本身,直接在栈上执行
- 然而,如果你在重写方法里调用base.ParentFunc(),内部依旧会装箱的!
- 调用引用基类的静态方法也不需要装箱,因为压根不依赖实例
1.3 为什么枚举是struct,但是却能继承class
- 从语法上,枚举声明为 enum TestEnum { … },编译器会自动把它编译为 struct TestEnum : Enum;
- 但 CLR 对枚举做了特殊处理:枚举的基类虽然是 Enum(class),但枚举实例本身是值类型,存储在栈上;
- 当需要将枚举转换为 Enum 类型(比如传给 HasFlag 的参数),就必须装箱 —— 因为引用类型参数只能接收堆上的对象。
1 | TestEnum a = TestEnum.Flag1; |
1.4 值类型没有方法表的解释
- 引用类型的方法调用逻辑(有方法表指针)
1 | 堆上的引用类型实例:[对象头(含方法表指针)] + [字段数据] |
- 值类型的方法调用逻辑(无方法表指针)
值类型实例存储在栈上,结构是:[字段数据](无对象头、无方法表指针),调用方法分两种情况:- 情况 1:调用值类型自定义 / 重写的方法(比如 A.ToString())
CLR 直接从程序集元数据中找到该方法的静态地址(编译期已确定);
把栈上值类型实例的地址(this)传给方法,直接执行;
全程不需要方法表指针,也不需要装箱 —— 这就是你定义的结构体方法能正常执行的原因。 - 情况 2:调用基类(object/ValueType) 的方法(比如 base.ToString())
基类方法需要依赖方法表执行(比如 object.ToString() 是虚方法);
但值类型实例没有方法表指针,因此 CLR 必须先把值类型装箱(堆上创建引用类型对象,带方法表指针);
通过装箱对象的方法表指针找到基类方法,执行调用。
- 情况 1:调用值类型自定义 / 重写的方法(比如 A.ToString())
总结:
- “值类型没有方法表” → 准确表述是:值类型实例(栈上)没有方法表指针,方法表不随实例分配(引用类型实例必须带方法表指针);
- 值类型可以定义 / 执行方法 → 因为自定义 / 重写方法的地址编译期已确定,CLR 直接通过静态地址调用,无需方法表;
- 调用基类(引用类型)方法时 → 必须先装箱(生成带方法表指针的堆对象),才能通过方法表找到并执行基类方法。
- 所以,struct没有方法表,不能被继承
2 Enum.IsDefined 优化(20260916)
Enum.IsDefined 的函数签名是 public static bool IsDefined(Type enumType, object value),同样会有装箱问题。
除了调用时的装箱,方法里面可能还会惰性申请数组和二分查找,虽然普通路径下不需要考虑,但是战斗里的 Update 热路径则不得不防。
现代 .NET 提供了无装箱的泛型重载 Enum.IsDefined<TEnum>(TEnum),不过多数 Unity 版本使用 .NET Standard 2.1 / .NET Framework 4.8 API profile,通常没有这个现代泛型重载。针对 Unity 这个情况,解决方法就是缓存:
1 | public static bool IsDefinedFast<T>(this T value) where T : struct, Enum |
简化版
1 | public static class EnumCache<T> where T : struct, Enum |
此外 Enum.IsDefined 对于 [Flags] 枚举,组合值如果没有被显式声明,Enum.IsDefined 会返回 false。