场景
开发的时候遇到一个需求, 要求在个人信息中增加个人爱好字段,个人爱好可选值有:运动、娱乐、探险、智力、收藏、乐器。
用枚举表达则为:
enum HobbyType {
/**运动 */
Sport,
/**娱乐 */
Entertainment,
/**探险 */
Adventure,
/**智力 */
Intelligence,
/**收集 */
Collection,
/**乐器 */
Musical,
}
爱好值是可叠加的,所以存储的数据我们可以用无重复集合来表达,这在代码执行时使用是没太大问题的,但是如果是需要将数据存储到数据库等不利于建表和查询。
关于建表,我们只需要一个字段,常见的做法是将集合转为字符串进行存储,比如使用连字符-将集合分隔开以便于存储:
const hobbyStr = [HobbyType.Sport, HobbyType.Musical].join('-');
使用时再转换回集合。
而这种方式并不方便查询,比如爱好包含运动的人,就需要查询所有信息并转换数据,再来逐一判断。
对于这种可选状态集合,使用二进制位运算中的与运算即可判断某一状态值是否为真。
利用位权重表示状态值的叠加效果
如果我们记录的状态值不是字符串,如何利用一个数字记录这些状态值而保证状态不重复呢?
我们发现,利用权重的特点进行加法运算,叠加值是不会重复的,以10进制为例,位就是我们常说的个位、十位、百位、千位等等。
我们将状态值设置为不同权重的位数,则上述案例中的枚举将变为:
enum HobbyType {
/**运动 */
Sport = 1,
/**娱乐 */
Entertainment = 10,
/**探险 */
Adventure = 100,
/**智力 */
Intelligence = 1000,
/**收集 */
Collection = 10000,
/**乐器 */
Musical = 100000,
}
由于位权重不一样,这些数字相互相加时绝不会出现重复值,比如11一定是1 + 10,110一定是10 + 100。
实际上,这里的位数和进制是一致的,扩展到计算机中,就是二进制数据,而计算机中的位权重也就是2,上面的状态值在代码中就应该这样表示:
enum HobbyType {
/**运动 */
Sport = 1,
/**娱乐 */
Entertainment = 2,
/**探险 */
Adventure = 4,
/**智力 */
Intelligence = 8,
/**收集 */
Collection = 16,
/**乐器 */
Musical = 32,
}
这样,我们就能直接存储状态值之和来表示状态的叠加效果。
利用与运算判断状态值是否为真
利用状态值之和表达状态叠加效果,反过来,如何判断某个状态是否为真才是我们的目的。
与运算十分便捷地解决了这一需求:使用待判断的状态值同状态叠加值进行与运算,如果结果大于1,说明该状态未真。
首先什么是与运算?
程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算就是直接对整数在内存中的二进制位进行操作。
我们可以把位运算理解为二进制加减运算的简版,其中与运算规则为:
两者同时为1时,值才为1。
为何进行与运算就能判断状态值为真?
为了方便直观理解,我们仍然以十进制为例,比如状态叠加值为1101 (= 1 + 100 + 1000),我们发现,状态值只有最高位为1,其余为均是0,根据与运算的规则,这些低位进行与运算得到的值都为0,那么变化只有最高位了:
- 如果最高位在叠加值中为1,那么与运算得到1,整体与运算的结果就是状态值本身。
- 如果最高位在叠加值中未0,那么与运算得到0,整体与运算结果为0.
由于每个状态的位权重都不一样,因此只有状态值是叠加值的加数时,才会出现情况1,由此得到状态值同状态叠加值进行与运算,结果大于1则状态为真,为0则状态为假的结论。
而与运算可以直接在查询语句中使用。
封装为工具类
为了更加方便地操作状态集合,我们可以根据上述理论转换为一个工具类,提供状态类的真假判断、状态添加、状态删除、状态叠加值、状态集合等方法的工具类。
参考代码(typescript):
/**状态缓存器 */
class StateCache {
/**
* 获取状态值
* @param weight 权重(≥0)
*/
static getState(weight: number) {
return Math.pow(2, weight);
}
/**
* 状态叠加值
*/
private totalState: number;
/**
*
* @param stateOrList 状态叠加值或状态列表
*/
constructor(stateOrList?: number | number[]) {
if(Array.isArray(stateOrList)) {
this.totalState = 0;
stateOrList.forEach(n => this.add(n));
} else {
this.totalState = stateOrList || 0;
}
}
/**状态叠加值 */
get state() {
return this.totalState;
}
/**为真的状态列表 */
get list() {
const list: number[] = [];
let weight = 0;
let state = StateCache.getState(weight);
while(state <= this.totalState) {
if(this.has(state)) {
list.push(state);
}
state = StateCache.getState(++weight);
}
return list;
}
/**
* 判断状态值是否为真
*/
has(state: number) {
return (state & this.totalState) > 0;
}
/**
* 添加状态
*/
add(state: number) {
if(!this.has(state)) {
this.totalState += state;
}
}
/**
* 删除状态
*/
del(state: number) {
if(this.has(state)) {
this.totalState -= state;
}
}
}
// ==>测试
enum HobbyType {
/**运动 */
Sport = StateCache.getState(0),
/**娱乐 */
Entertainment = StateCache.getState(1),
/**探险 */
Adventure = StateCache.getState(2),
/**智力 */
Intelligence = StateCache.getState(3),
/**收集 */
Collection = StateCache.getState(4),
/**乐器 */
Musical = StateCache.getState(5),
}
const hobbyState = new StateCache([HobbyType.Sport, HobbyType.Musical]);
console.log(hobbyState.has(HobbyType.Musical)); // true
console.log(hobbyState.has(HobbyType.Intelligence)); // false
hobbyState.add(HobbyType.Intelligence);
console.log(hobbyState.has(HobbyType.Intelligence)); // true
hobbyState.del(HobbyType.Sport);
console.log(hobbyState.has(HobbyType.Sport)); // false
console.log(hobbyState.state, '=>' , hobbyState.list); // 40 => [ 8, 32 ]