目录

C#-位标志.md

一、什么是位标志

核心思想:一个整数的每一个二进制 = 一个独立的"是 / 否"开关

解决的问题:

当一个变量需要同时表示多个状态的任意组合时(比如用户权限、文件访问模式、游戏角色 Buff 状态),用位标志可以用一个整数代替多个bool字段。

二、为什么枚举值必须是1,2,4,8……?

因为它们在二进制中各占独立的一位,互不冲突:

1  = 00000001  ← 第1位    0x01
2  = 00000010  ← 第2位	0x02
4  = 00000100  ← 第3位	0x04
8  = 00001000  ← 第4位	0x08
16 = 00010000  ← 第5位	0x10

如果用1,2,3,4……会出现歧义:

3 = 0011
   这和 Read(0001) + Write(0010) 的组合完全相同!
   你无法分辨"3"到底是独立的值,还是1和2的组合。

三、如何定义位标志枚举?

[Flags]
enum Permission
{
    None = 0;			// 没有任何权限(特殊值,固定为0)
    
    // 使用左移运算符,清晰地标明每个值占第几位
    Read    = 1 << 0,	// 00000001 = 1
    Write   = 1 << 1,	// 00000010 = 2
    Delete  = 1 << 2,	// 00000100 = 4
    Execute = 1 << 3,	// 00001000 = 8
    
    // 预定义组合权限(可选)
    ReadWirte = Read | Write,
    All       = Read | Write | Delete | Execute
}

[Flags]特性地作用

[Flags] 不影响位运算本身,只影响 TosString() 的输出,让调试更易读:

// 不加 [Flags]
enum Perm { Read = 1, Write = 2 }
Perm p = (Perm)3;
Console.WriteLine(p);   // 输出:3        ← 看不懂

// 加了 [Flags]
[Flags]
enum Perm { Read = 1, Write = 2 }
Perm p = (Perm)3;
Console.WriteLine(p);   // 输出:Read, Write  ← 清晰明了

左移运算符 « 的好处

// 手动计算,容易写错
Read    = 1,
Write   = 2,
Delete  = 4,
Execute = 8,
Admin   = 16,
Super   = 32,
// 如果有几十个枚举值,靠手算极易出错

// 使用左移,清晰且不会算错
Read    = 1 << 0,   // 明确表示"第0位"
Write   = 1 << 1,   // 明确表示"第1位"
Delete  = 1 << 2,   // 明确表示"第2位"
Execute = 1 << 3,   // 明确表示"第3位"

四、四种核心操作

操作 运算符 记忆口诀 示例
添加 p | = X 加上 p |= Permission.Read
移除 p &= ~X 去掉 p &= ~Permission.Read
判断 p.HasFlag(X) 有没有 p.HasFlag(X)
切换 p ^ = X 翻转开关 p ^= Permission.Read

添加权限( | 或运算)

Permission p = Permission.None;   // 0000

p |= Permission.Read;             // 0000 | 0001 = 0001
p |= Permission.Write;            // 0001 | 0010 = 0011

Console.WriteLine(p);             // 输出:Read, Write

原理: 或运算 ( | ): 有一个 1 结果就是 1,相当于"合并"两个状态

  0001  (Read)
| 0010  (Write)
──────
  0011  (Read + Write)

移除权限( & 与运算,~ 取反)

Permission p = Permission.Read | Permission.Write;		// 0011

p &= ~Permission.Write;		// 移除 Write

// 执行过程
// Write	= 0010
// ~Write 	= 1101 (取反: 0 变 1, 1 变 0)
// p & ~Write = 0011 & 1101 = 0001 (只剩 Read)

Console.WriteLine(p);		//  输出: Read

**原理:**先用 ~ 把目标位取反, 再用 & 把那一位强制清零,其它位保持不变。(& 按位与运算:只有两个对应都是1,结果才是1;否则为0。)

  0011  (Read + Write)
& 1101  (~Write)
──────
  0001  (只剩 Read)

判断是否包含某权限(HasFlag 或 &)

Permission p = Permission.Read | Permission.Delete;  // 0101

// 方式1:HasFlag(推荐,语义清晰)
if (p.HasFlag(Permission.Read))
    Console.WriteLine("✅ 有读权限");

if (!p.HasFlag(Permission.Write))
    Console.WriteLine("❌ 没有写权限");

// 方式2:手动用 & 判断(等价写法)
if ((p & Permission.Read) == Permission.Read)
    Console.WriteLine("✅ 有读权限");

原理:& 运算会把不相关的位全部清零,只保留目标位

  0101  (Read + Delete)
& 0001  (Read)
──────
  0001  ← 结果不为0,说明包含 Read ✅

  0101  (Read + Delete)
& 0010  (Write)
──────
  0000  ← 结果为0,说明不包含 Write ❌

切换权限(^ 异或运算)

Permission p = Permission.Read;   // 0001

p ^= Permission.Write;   // 没有 Write → 加上
Console.WriteLine(p);    // 输出:Read, Write  (0011)

p ^= Permission.Write;   // 有 Write → 去掉
Console.WriteLine(p);    // 输出:Read  (0001)

**原理:**异或(^):相同为0,不同为1。对同一位异或两次等于没做。

第一次(加上):
  0001  (Read)
^ 0010  (Write)
──────
  0011  (Read + Write)

第二次(去掉):
  0011  (Read + Write)
^ 0010  (Write)
──────
  0001  (只剩 Read)

五、示例

[Flags]
enum Permission
{
    None    = 0,
    Read    = 1 << 0,
    Write   = 1 << 1,
    Delete  = 1 << 2,
    Execute = 1 << 3,
    All     = Read | Write | Delete | Execute
}

class Program
{
    static void Main()
    {
        // ① 初始化:读 + 写
        Permission p = Permission.Read | Permission.Write;
        Console.WriteLine($"初始:{p}");
        // 输出:初始:Read, Write

        // ② 添加:删除权限
        p |= Permission.Delete;
        Console.WriteLine($"添加删除后:{p}");
        // 输出:添加删除后:Read, Write, Delete

        // ③ 移除:写权限
        p &= ~Permission.Write;
        Console.WriteLine($"移除写后:{p}");
        // 输出:移除写后:Read, Delete

        // ④ 判断:有没有读权限
        Console.WriteLine($"有读权限:{p.HasFlag(Permission.Read)}");
        // 输出:有读权限:True

        Console.WriteLine($"有写权限:{p.HasFlag(Permission.Write)}");
        // 输出:有写权限:False

        // ⑤ 切换:执行权限(没有 → 加上)
        p ^= Permission.Execute;
        Console.WriteLine($"切换执行后:{p}");
        // 输出:切换执行后:Read, Delete, Execute

        // ⑥ 切换:执行权限(有 → 去掉)
        p ^= Permission.Execute;
        Console.WriteLine($"再次切换后:{p}");
        // 输出:再次切换后:Read, Delete
    }
}

六、应用场景

场景 示例
用户权限管理 Permission.Read | Permission.Write
文件访问模式 FileAccess.Read | FileAccess.Write
正则表达式选项 RagexOptions.IgnoreCase | RagexOptions.Multiline
WPF/Winforms 控件锚定 AnchorStyles.Top | AnchorStyles.Left
游戏角色状态 BuffType.Poison | BuffType.Slow | BuffType.Stun
数据库存储多选项 用一个 int 列存储多个勾选状态

七、常见错误与注意事项

忘记 None = 0

// 错误:没有 None = 0
[Flags]
enum Permission { Read = 1, Write = 2 }

// 当变量没有任何权限时,值是 0,但 0 没有对应的枚举项
// ToString() 会输出 "0" 而不是有意义的名字

枚举值不是2的幂次方

// 错误:3 会和 Read + Write 的组合冲突
[Flags]
enum Permission { Read = 1, Write = 2, Custom = 3 }

判断时忘记加括号

// 错误:& 的优先级低于 ==,不加括号结果不对
if (p & Permission.Read == Permission.Read)   // ❌

// 正确
if ((p & Permission.Read) == Permission.Read) // ✅
// 当然,直接用 HasFlag 就不会有这个问题
if (p.HasFlag(Permission.Read))               // ✅ 最推荐

八、总结

位标志三要素:

① 定义:[Flags] + 枚举值必须是 1, 2, 4, 8...(2的幂次方)
          推荐用左移运算符 << 来写,清晰不易错

② 操作:只需掌握四个运算
          加上  →  |=
          去掉  →  &= ~
          判断  →  HasFlag()
          切换  →  ^=

③ 本质:用一个整数的每一个二进制位
          代表一个独立的"是/否"开关

“位标志 = 用一个整数的二进制位当开关,| 加, &~ 减, HasFlag 判断,^ 翻转。”