mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1993 字
5 分钟
位掩码(Bitmask)
2026-06-13

一、为什么需要位掩码#

假设你在写一个权限系统,每个用户可以拥有「读」「写」「执行」三种权限。最直觉的做法是用三个布尔变量:

let canRead = true;
let canWrite = false;
let canExecute = true;

三个变量还好,但如果权限扩展到 20 种呢?你要声明 20 个布尔变量,写 20 个条件判断,传参时拖着 20 个参数。更头疼的是,如果你想判断「用户是否同时拥有读和写权限」,代码变成了冗长的逻辑与:

if (canRead && canWrite && canExecute) {
// ...
}

再想想序列化的场景。假设你要把这些权限存到数据库或通过网络传输,20 个布尔字段意味着 20 列或者 20 个 JSON key。而实际上,20 个布尔值只需要 20 个比特位,一个 32 位整数就够了——一个 4 字节的 int 就能装下 32 个开关

位掩码就是解决这类问题的模式:把多个布尔标志压缩到一个整数中,用位运算来完成设置、检查、清除和切换,所有操作都是 O(1) 常数时间,而且不需要额外的内存分配。

二、现实类比#

想象酒店房间的钥匙卡。老式酒店每扇门一张卡,住五天领五张卡,口袋塞得满满的。现代酒店把所有房间的权限编码到一张卡里——卡里某个比特位是 1,对应房间就能开。前台加权限就是「把某个位设为 1」,退房就是「把位清零」,一张卡搞定所有门禁。

三、核心思想#

位掩码的核心操作只有四种:设置(set)、检查(check)、清除(clear)和切换(toggle)。每个操作对应一条位运算指令,CPU 周期固定,不随标志数量增长。

flowchart LR A["标志位定义\nRead=1<<0\nWrite=1<<1\nExec=1<<2"] --> B["打包为整数\nmask = 0b101"] B --> C{"位运算操作"} C -->|"设置 \|= flag"| D["mask |= Write\n→ 0b111"] C -->|"检查 & flag"| E["mask & Read\n→ 0b001 ≠ 0"] C -->|"清除 &= ~flag"| F["mask &= ~Exec\n→ 0b001"] C -->|"切换 ^= flag"| G["mask ^= Read\n→ 0b000"]

3.1 四种基本操作#

操作位运算含义示例(mask=0b101
设置mask |= flag将某位设为 1mask |= Write0b111
检查mask & flag判断某位是否为 1mask & Read → 非零即真
清除mask &= ~flag将某位设为 0mask &= ~Exec0b001
切换mask ^= flag0 变 1,1 变 0mask ^= Read0b000

3.2 组合操作#

单个标志的运算只是基础,位掩码真正的威力在于组合操作——用一次运算完成多个标志的判断。

操作位运算含义
组合多个标志A | B | C一次设置多个位
检查全部匹配(mask & flags) === flags所有指定位都为 1
检查任意匹配(mask & flags) !== 0至少有一个指定位为 1
清除多个标志mask &= ~flags一次清除多个位

3.3 复杂度#

操作时间复杂度空间复杂度
设置 / 检查 / 清除 / 切换O(1)O(1)
组合标志O(1)O(1)
遍历所有已设置的位O(n)O(1)

其中 n 是整数位数(32 或 64),而非标志的语义数量。

四、变体与对比#

位掩码不是万能的。它和几种相关模式各有适用场景:

模式核心思路适用场景不适用场景
位掩码整数比特位编码布尔标志多个独立开关、权限、状态组合标志互斥、超过 32/64 个
枚举(Enum)整数值编码互斥状态状态机、类型标记需要同时持有多个状态
标签集合(Tag Set)哈希集合存储标签字符串动态标签、数量不确定高频热路径、需要序列化
标记联合(Tagged Union)紧凑整数编码类型 + 联合体载荷多态数据结构纯布尔标志
脏标记(Dirty Flag)通常用位掩码存储变更字段增量更新、部分写回标志之间有依赖关系

位掩码 vs 枚举是最容易混淆的。关键区别:枚举值互斥(一个变量只能等于一个枚举值),位掩码值可叠加(一个整数可以同时包含多个标志)。如果你发现自己写了 state === (Running | Paused),说明这两个状态不应该是位标志——一个进程不可能同时运行又暂停。

五、多语言实现#

5.1 Go 实现#

Go 语言的 iota 让位掩码的定义格外简洁,os.FileMode 就是这么做的。

package bitmask
// 用 iota 定义标志位,每个标志占一个比特位
const (
Read uint32 = 1 << iota // 1 << 0 = 0b001
Write // 1 << 1 = 0b010
Execute // 1 << 2 = 0b100
)
type Permission uint32
// 设置标志
func (p *Permission) Set(flag uint32) {
*p |= Permission(flag)
}
// 检查标志
func (p Permission) Has(flag uint32) bool {
return p&Permission(flag) != 0
}
// 清除标志
func (p *Permission) Clear(flag uint32) {
*p &= ^Permission(flag)
}
// 切换标志
func (p *Permission) Toggle(flag uint32) {
*p ^= Permission(flag)
}
// 检查是否同时拥有所有指定标志
func (p Permission) HasAll(flags uint32) bool {
return p&Permission(flags) == Permission(flags)
}

使用方式:

var perm Permission
perm.Set(Read | Execute) // 同时设置读和执行
perm.Has(Write) // false
perm.Set(Write) // 加上写权限
perm.HasAll(Read | Write) // true
perm.Clear(Execute) // 去掉执行权限
perm.Toggle(Read) // 读权限切换:有 → 无

5.2 TypeScript 实现#

JavaScript 的位运算是 32 位有符号整数,这一点很容易踩坑。超过第 31 位的标志会因符号位溢出而产生意外结果。

// 用左移定义标志位
const READ = 1 << 0; // 0b001
const WRITE = 1 << 1; // 0b010
const EXECUTE = 1 << 2; // 0b100
type Permission = number;
// 设置标志
function set(mask: Permission, flag: number): Permission {
return mask | flag;
}
// 检查标志——注意用 & 而非 ===
function has(mask: Permission, flag: number): boolean {
return (mask & flag) !== 0;
}
// 清除标志
function clear(mask: Permission, flag: number): Permission {
return mask & ~flag;
}
// 切换标志
function toggle(mask: Permission, flag: number): Permission {
return mask ^ flag;
}
// 检查是否同时拥有所有指定标志
function hasAll(mask: Permission, flags: number): boolean {
return (mask & flags) === flags;
}
Warning

JavaScript 位运算的 32 位限制

1 << 32 在 JavaScript 中等价于 1 << 0,结果还是 1。移位运算只取操作数的低 5 位(0-31)作为移位量。如果你的标志超过 31 个,位运算会悄悄回绕,不会报错。超过 32 个标志时,应该改用 SetBigInt

另一个常见陷阱是混淆 ===&

// 错误:只在权限恰好等于 READ 时才为 true
perm === READ
// 正确:只要包含 READ 位就为 true
(perm & READ) !== 0

perm === READ 只有当权限恰好是读、没有任何其他标志时才返回 true。如果你只想检查「有没有读权限」,必须用按位与。

六、生产验证#

位掩码不是纸上谈兵的模式,下面是三个真实项目中的使用案例。

6.1 React — Fiber 副作用标志#

React 的 Fiber 架构用位掩码追踪组件树上的副作用(插入、更新、删除、引用更新等)。每个 Fiber 节点有一个 flags 字段,用 |= 标记需要处理的副作用,渲染阶段用 & 检查。

6.2 Linux 内核 — 文件权限#

Linux 用 9 个比特位表示经典的 rwxrwxrwx 权限模型——属主、属组、其他各 3 位。chmod 755 本质上就是在设置一个 12 位的位掩码(包含 setuid/setgid/sticky 位)。

  • 项目:torvalds/linux
  • 源码:include/uapi/linux/stat.h
  • 定义了 S_IRUSRS_IWUSRS_IXUSR 等权限常量(第 14-36 行),内核在 inode 和 dentry 操作中广泛使用位运算检查权限

6.3 Go 标准库 — FileMode#

Go 的 os.FileMode 用位掩码编码文件类型和权限信息,包括 Unix 权限位、setuid/setgid/sticky 位、符号链接、设备文件等。

  • 项目:golang/go
  • 源码:src/os/types.go
  • ModeDirModeAppendModeExclusive 等常量(第 33-50 行)重新导出自 fs.FileModeos.Stat() 返回的 FileInfo.Mode() 就是位掩码,通过 mode&os.ModeDir != 0 判断文件类型

七、小结#

适合使用位掩码的场景

  • 热路径上有多个独立的布尔标志,需要 O(1) 的设置和检查
  • 权限系统或配置项需要组合判断(「同时拥有 A 和 B」或「至少拥有一个」)
  • 网络传输或持久化时需要紧凑的序列化格式
  • ECS(Entity Component System)中用位掩码匹配组件集合

不适合使用位掩码的场景

  • 标志数量超过 32 个(JavaScript)或 64 个(Go/C),位运算会溢出或回绕
  • 标志之间互斥(状态只能取一个值),应该用枚举代替
  • 可读性比性能更重要,Set<string> 比位运算直观得多
  • 标志集合是动态的、运行时才能确定,位掩码要求编译期就知道有哪些标志

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

位掩码(Bitmask)
https://blog.souloss.com/posts/programming/data-structures/data-structures-bitmask/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时