很多场景下,我们会使用 bool 值来描述实体状态,通常用实体的一个属性来表示。有时对于同一个实体有多个独立的 bool 状态,需要很多标记位,比如在文档协作工具中,用户对于一篇文档的权限,可能包含是否可读、是否可写、是否可评论、是否可删除等。
class FilePermission {
public canRead: boolean;
public canWrite: boolean;
public canComment: boolean;
public canDelete: boolean;
}
对于多个 bool 状态标记,我们可以使用位标记来替换它,仅使用一个属性来表示实体下所有的 bool 状态。这样做的好处是,一方面减少实体的属性,代码层面会更简洁;另一方面,单标记比多标记更节省空间,在数据存盘时能节约大量磁盘空间。
比如,新功能引导是一个非常适合使用位标记的场景:用户初次体验新功能时,给用户一个引导流程,后续再使用到该功能,则无需再引导。很明显,对于长期迭代的产品,不断会有新功能上线,用户是否访问过某功能,就可以使用位标记来记录。
位标记介绍
通过将每个布尔值映射到整数的一个二进制位,可以在一个整数中存储多个标志(flags)。位标记在计算机科学中广泛应用于各种场景,特别是在需要高效管理和查询多个布尔状态的情况下。
基本概念
- 位:一个位是二进制数字中的最小单位,可以是 0 或 1。
- 位标记:一个整数,其中每个位代表一个布尔值。
- 位操作:使用位运算符(如按位与
&
、按位或|
、按位异或^
、按位取反~
、左移<<
、右移>>
)来操作位标记。
优点
- 内存效率:位标记占用的内存非常少。例如,一个 32 位整数可以表示 32 个布尔值。
- 性能高效:位操作通常非常快,因为它们直接在硬件级别执行。
- 简洁性:位标记可以用一个整数表示多个状态,使代码更加简洁。
原理
假设我们有一个 8 位的整数(1 字节),可以表示 8 个布尔值:00000000,每个位的位置从右到左编号为 0 到 7:7 6 5 4 3 2 1 0。我们可以用每一位表示一个标识。如对于文档权限,可读、可写、可评论、可删除分别用从右到左 0 ~ 3 位表示。则
- 假如用户仅可读,可以用二进制 00000001 表示,对应十进制 1。
- 用户仅可读、可评论,可用二进制 00000101 表示,对应十进制 5。
位操作
使用位运算来完成二进制位的设置和取值。
设置位
使用按位或、按位与、位取反来设置位。假设我们要将第 0 位和第 2 位设置为 1:
// 使用按位或 ”|“ 来设置某位为 1
let bits = 0b00000000; // 初始值
bits |= (1 << 0); // 设置第 0 位
bits |= (1 << 2); // 设置第 2 位
console.log(bits); // 5
// 使用按位与、位取反来设置某位为 0
bits &= ~(1 << 2); // 设置第 2 位为 0
console.log(bits); // 1
检查位
使用按位与来检查位。比如检查第二位是否为 1:
const isSet = (bits & (1 << 2)) !== 0; // 检查第 2 位
生产环境中使用位标记来存储状态
实现一个通用工具类
如前所述,我们可以使用一个整数来描述多个状态,当然整数的位数是有限的,用单个整数并不能表示无限多个状态,因为会溢出。这种情况下,我们可以使用多个整数来表示状态。
运用前面介绍的原理,可以简单实现一个无限状态位的工具类 BitMarker,内部使用数组来存储所有标记:
const INT_MAX_BIT = 32;
/**
* 位标记类,用于管理和操作位集合。
*/
export class BitMarker {
private nums: number[] = []; // 存储位集合的整数数组
private bitCount: number = 0; // 当前位集合的总位数
/**
* 构造函数,初始化位标记对象。
* @param nums 初始的整数数组,可以包含 0 到多个正整数,用于设置位集合的初始值
*/
constructor(nums: number[] = []) {
this.setValues(nums);
}
/**
* 设置指定位的标记。
* @param bitIndex 指定位 0 ~ n
* @param value 要设置的标记
*/
public set(bitIndex: number, value: boolean) {
if (bitIndex < 0) {
return; // 忽略负索引
}
if (bitIndex >= this.bitCount) {
this.resize(bitIndex + 1); // 如果索引超出当前容量,调整内部数组大小
}
const numIndex = Math.floor(bitIndex / INT_MAX_BIT); // 计算整数数组的索引
const bitOffset = bitIndex % INT_MAX_BIT; // 计算位偏移量
const mask = 1 << bitOffset; // 创建掩码
if (value) {
this.nums[numIndex] |= mask; // 设置位为 1
} else {
this.nums[numIndex] &= ~mask; // 清除位为 0
}
}
/**
* 根据指定位的标记
* @param bitIndex 指定位 0 ~ n
* @returns 返回该位的标记
*/
public get(bitIndex: number): boolean {
const numIndex = Math.floor(bitIndex / INT_MAX_BIT); // 计算整数数组的索引
const bitOffset = bitIndex % INT_MAX_BIT; // 计算位偏移量
const mask = 1 << bitOffset; // 创建掩码
return (this.nums[numIndex] & mask) !== 0; // 检查位是否为 1
}
/**
* 清空所有标记
*/
public clear() {
this.nums = []; // 清空整数数组
this.bitCount = 0; // 重置位计数
}
/**
* 设置位标记的值
* @param values 初始的整数数组,用于设置位集合的初始值
*/
public setValues(values: number[]) {
this.nums = values; // 设置新的整数数组
this.bitCount = values.length * 32; // 更新位计数
}
/**
* 获取当前位标记的值
* @returns 返回当前位集合的整数数组
*/
public getValues(): number[] {
return this.nums; // 返回整数数组
}
/**
* 调整位标记数组的大小。
* @param newSize 新数组需要支持的位数
*/
private resize(newSize: number) {
const numsCount = Math.ceil(newSize / INT_MAX_BIT); // 计算需要的整数数组长度
while (this.nums.length < numsCount) {
this.nums.push(0); // 填充新的整数
}
this.bitCount = newSize; // 更新位计数
}
}
使用示例:
/*
* 有新功能上线时,出现功能引导,每个功能仅出现一次。
* 使用 BitMarker 对出现过的引导状态进行标记。
* 每一位表示一个功能的引导状态。
*/
enum FunctionGuide {
A = 0, // 功能 A
B = 1, // 功能 B
C = 2,
D = 3,
}
// 1. 初始化
const initStatus = []; // 初始标记,从数据库读出或者初始化
const bitMarker = new BitMarker(initStatus);
// 2. 设置标记
// 功能 A 已引导
bitMarker.set(FunctionGuide.A, true);
// 3. 获取标记
const isGuideA = bitMarker.get(FunctionGuide.A);
console.log(isGuideA); // true
const isGuideB = bitMarker.get(FunctionGuide.B);
console.log(isGuideB); // false
// 4. 获取标记结果
const bits = bitMarker.getValues(); // 数组格式,可以序列化后存库
直接使用 npm 包
上面的的 BitMarker 已经发布为 npm 包(bit-marker)了,可以安装后直接使用。
安装
npm install bit-marker
API
- constructor(initStatus: number[])
初始化,initStatus 为初始标记,从数据库读出或者初始化 - set(bit: number, value: boolean)
设置标记,bit 为标记位,value 为标记值,true 为已标记,false 为未标记。 - get(bit: number)
获取标记,bit 为标记位 - setValues(values: number[])
接收一个整数数组,每个元素有效值为 [0, 2 ^ 32 - 1],每个数字表示 32 个标记位,当标记数超过 32 时,会自动扩展。 - getValues()
获取所有标记组成的数组。 - clear()
清空所有标记。