利用位运算存储可选状态值

1,104 阅读4分钟

场景

开发的时候遇到一个需求, 要求在个人信息中增加个人爱好字段,个人爱好可选值有:运动、娱乐、探险、智力、收藏、乐器。

用枚举表达则为:

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 + 10110一定是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,那么与运算得到1,整体与运算的结果就是状态值本身。
  2. 如果最高位在叠加值中未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 ]