1 优化位枚举 1.1 原生HasFlag问题
原生的HasFlag方法会将枚举装箱为Enum类型,产生性能开销
Enum本身是一个引用类型,所有具体的枚举类型比如MyColor都是值类型,并且继承自Enum(特殊的继承规则,就像所有C#对象都继承自object,它也是引用类型)。当调用基类Enum的方法时,自身需要装箱成Enum,参数也需要传递给Enum flag,会进行两次装箱
HasFlagFast 用泛型值类型参数替代 Enum 引用类型参数,从根源上避免了装箱。Unsafe.As 只是进一步优化了类型转换的性能,而泛型值类型参数才是避免装箱的关键
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public bool HasFlag (Enum flag ) { } public static bool HasFlagFast <T >(this T flags, T value ) where T : struct , Enum{ if (Enum.GetUnderlyingType(typeof (T)) != typeof (int )) throw new ArgumentException($"枚举类型 {typeof (T).Name} 的底层类型不是 Int32,无法使用此方法" , nameof (T)); var f = Unsafe.As<T, int >(ref flags); var v = Unsafe.As<T, int >(ref value ); return (f & v) == v; }
1.2 内部发生了什么 1 bool result1 = myEnum.HasFlag(TestEnum.Flag2);
通过SharpLab查看IL如下,发现进行了两次装箱
1 2 3 4 5 6 IL_0003: ldloc.0 IL_0004: box TestEnum IL_0009: ldc.i4.2 IL_000a: box TestEnum IL_000f: call instance bool [System.Runtime]System.Enum::HasFlag(class [System.Runtime]System.Enum) IL_0014: stloc.1
不只是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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 TestEnum a = TestEnum.Flag1; TestEnum b = TestEnum.Flag1; bool isEqual1 = a.Equals(b); bool isEqual2 = ReferenceEquals((Enum)a, (Enum)b); Console.WriteLine(isEqual1); Console.WriteLine(isEqual2); enum TestEnum{ Flag1, Flag2 }
1.4 值类型没有方法表的解释
1 2 3 4 5 6 堆上的引用类型实例:[对象头(含方法表指针)] + [字段数据] 调用方法时: 1 . 通过对象头的方法表指针找到类型的方法表;2 . 从方法表中找到要调用的方法地址;3 . 执行方法。哪怕是空对象(new object ()),对象头也会占用 8 /16 字节(取决于 32 /64 位),核心就是存方法表指针。
值类型的方法调用逻辑(无方法表指针) 值类型实例存储在栈上,结构是:[字段数据](无对象头、无方法表指针),调用方法分两种情况:
情况 1:调用值类型自定义 / 重写的方法(比如 A.ToString()) CLR 直接从程序集元数据中找到该方法的静态地址(编译期已确定); 把栈上值类型实例的地址(this)传给方法,直接执行; 全程不需要方法表指针,也不需要装箱 —— 这就是你定义的结构体方法能正常执行的原因。
情况 2:调用基类(object/ValueType) 的方法(比如 base.ToString()) 基类方法需要依赖方法表执行(比如 object.ToString() 是虚方法); 但值类型实例没有方法表指针,因此 CLR 必须先把值类型装箱(堆上创建引用类型对象,带方法表指针); 通过装箱对象的方法表指针找到基类方法,执行调用。
总结:
“值类型没有方法表” → 准确表述是:值类型实例(栈上)没有方法表指针,方法表不随实例分配(引用类型实例必须带方法表指针);
值类型可以定义 / 执行方法 → 因为自定义 / 重写方法的地址编译期已确定,CLR 直接通过静态地址调用,无需方法表;
调用基类(引用类型)方法时 → 必须先装箱(生成带方法表指针的堆对象),才能通过方法表找到并执行基类方法。
所以,struct没有方法表,不能被继承