合法Flags枚举必须显式赋值2的幂(如Read=1, Write=2, Execute=4),含None=0,禁用非幂值;组合用|、检测用&而非HasFlag(),尤其避免在LINQ to Entities中调用HasFlag()。

直接说结论:用 [Flags] 标记枚举,每个成员必须是 2 的幂(1、2、4、8…),组合用 |,检测用 & 或 HasFlag(),但别在 LINQ to Entities 里调 HasFlag()。
怎么声明一个合法的 Flags 枚举
关键不是加了 [Flags] 就能用,而是底层二进制不能重叠。否则 Read | Write 和某个叫 ReadWrite 的非法值会算出同一个整数,调试时根本分不清。
- 必须显式赋值,且每个值是 2 的幂:
Read = 1 、Write = 1 、Execute = 1 - 一定要定义
None = 0,它不只是语义占位——它是默认值、清空操作的基础、序列化/反序列化的安全起点 - 禁止写成
A = 1, B = 2, C = 3:因为3的二进制是0011,和A | B完全等价,Enum.ToString()会输出"A, B"而不是"C",造成逻辑混乱
组合、设置、清除标志的正确写法
位运算不是炫技,是确保状态可叠加、可分离的唯一方式。别用布尔字段堆权限,也别手动维护 int 数字。
- 组合多个权限:
var perms = Permission.Read | Permission.Write | Permission.Execute - 添加一个新权限(不覆盖原有):
perms |= Permission.Delete - 移除某个权限:
perms &= ~Permission.Write(注意是~Permission.Write,不是!Permission.Write) - 清空所有:
perms = Permission.None或perms = 0(二者等价,但前者语义清晰)
检测是否包含某标志:>= vs HasFlag() vs 扩展方法
HasFlag() 看起来最直觉,但它有隐藏成本:装箱 + 反射,在热路径或高频循环中会影响性能;更严重的是,在 EF Core 的 IQueryable 中调用它,会导致整个查询被拉到内存执行(客户端评估),可能查出几万条再过滤。
- 推荐写法(高效、可服务端翻译):
(perms & Permission.Read) == Permission.Read - 可读性折中方案(需自己写扩展):
perms.Has(Permission.Read),内部仍用&运算,避免装箱 - 绝对要避开:
perms.HasFlag(Permission.Read)在.Where()、.First()等 LINQ 方法中出现
ToString() 和序列化行为容易踩坑
[Flags] 枚举重写了 ToString(),这是双刃剑。比如 (Permission.Read | Permission.Write).ToString() 返回 "Read, Write",看着很爽,但一旦你没按规范定义(比如漏了 None 或用了非幂值),它就可能返回空字符串、数字,甚至抛异常。
- JSON 序列化(如 System.Text.Json)默认输出字符串形式,但若字段是
int类型或配置了JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()),才生效 - 如果 API 需要兼容旧系统传数字,别依赖
ToString(),显式转(int)perms - 反序列化时,如果 JSON 是
"Read, Write",System.Text.Json 默认能解析;但如果是6(即Read | Write的数值),也必须能接受——这就要求你的枚举定义干净,没有歧义值
真正难的不是写对那几行位运算,而是让整个团队都理解:每个值为什么必须是 2 的幂,None = 0 为什么不是可选项,以及为什么 HasFlag() 在数据层是个危险信号。这些地方一松懈,后期 debug 权限错乱、接口返回空字符串、EF 查询变慢,全得回溯枚举定义本身。










