使用位运算存储用户状态

原创
03/09 10:44
阅读数 122

背景

很多项目中都会存储用户状态,诸如用户类型、相关操作权限等等。

比较常用的方案有两种。

  • 使用一个int类型字段存储用户状态,不同的数字代表不同的状态
  • 比较特殊的状态需要另外使用字段存储

方案一的问题在于无法存储复合状态。例如某个用户既是普通用户又处于封禁状态,想要使用方案一存储则数字会相当的多,不利于开发记忆。

因此我使用位运算方案来存储状态

思路

位运算的思路为利用一个int(或者long)的不同位来存储不同状态。

例如 状态数字 10,换算成二进制长这样:

1010

这样一看,一个数字 10 就有四个位,可以存储四个状态。

这个状态就可以这样解释:

最低位的0代表这是一个普通用户
第二低的1代表用户未曾登录
第二高的0代表用户可以使用手机号登录
最高位的1代表用户要接收消息通知

当然,每一位代表的意义在此并不重要。重要的是,每一个bit有两种状态:0和1,需要定义的是0代表enable还是1代表disable。这影响的是每一个bit的实际意义,也和接下来的方法定义有关。


同时,为了后续扩展,在进行状态定义的时候,最好要从低位往高位定义,高位未定义时必定为0。当用户需要增加新状态时,将新状态定义为1,那么原本的用户的状态数据可以不作修改。


存储效率:

以int类型为例,int占4个字节,共有4x8=32位,意为可以同时存储32种不同的状态,在实际意义上则代表2^32种现实意义

实现

定义

首先,
需要对要用到的位进行定义。

在此使用枚举来进行定义;

同时,
需要标明状态被定位在哪一个bit上

这一点很容易实现,使用2的幂指数即可;比如2=2^1,代表定位到从低往高数第二位的位置上

因此枚举定义如下:


public enum UserStatus {

    //以下均为代表不可用状态

    USERNAME(1),//是否可以使用username字段登录
    MOBILE(2),//是否可以使用手机号登录
    SMSCODE(4),//是否可以通过短信验证码登录
    SIGN(8),//是否可签署合同
    SEND(16),//是否发送合同
    INIT_PASSWORD(32),//是否是初始密码,0代表是初始密码,一个用户刚注册或导入的情况下都是初始密码(默认密码)
    INIT_SIGN_PASSWORD(64),//是否是默认的签署密码,0代表是默认密码
    DIY_VERIFY(128),//是否是修改过的的身份认证信息
    RECEIVE_SMS(256),//接收短信通知
    RECEIVE_EMAIL(512),//接收邮件通知
    RECEIVE_WEB_NOTICE(1024),//接收站内通知
    ;

    int code;//需要指定的位数,应该都是2的幂指数,指定位上为1代表不允许
    boolean userDiy;//是否允许普通用户自己修改指定状态

    UserStatus(int code) {
        this.code = code;
    }

    public int code() {
        return code;
    }


}



接下来需要对于状态的一些操作函数。我一共定义了三种:isenabledisable

is函数

首先是is函数:

方法申明如下:


boolean is(int code, BitInfo status);

其中:code代表存储了复合状态的状态数据,status代表某种定义的状态,我再次定义了一个BitInfo接口用于扩展,简单的理解成一个2的幂指数即可。

那么is函数的意义为:在code中的status对应位上是否为1

我使用的是 对应位 为 1,实际上0或者1不影响方法定义,只会对使用上有影响


接下来就是位运算的部分了

可以把这个函数实现分成两部分:一、取出指定位的数据;二、判断数字

还是拿 10 举例,在此我需要判断其从低往高数第二位是否为0
那么code传递的应该是10,status代表的幂指数应该是2

10化为二进制如下

1010

2化为二进制如下

0010

很明显可以看出,只需要将两个数字进行与(&)运算,即可以拿出指定位;准确的说是将无关位置为0,指定位不变。

结果是:

0010

再与2做个比对即可。


因此方法实现如下:

boolean is(int code, BitInfo status) {
     //和指定位进行与运算后,对应的结果为0,即为允许,status.code 为1,代表不允许
    return (code & status.code()) != status.code();
}


disable

disable函数的意义是:将指定位置为1。

要求:

  • 随便一个状态数字在disable之后进行is返回false
  • 对状态数字进行反复disable,结果都不能有变化(意思是不能简单的使用取反操作)

方法申明如下

int disable(int code, BitInfo status);

在此将10的最低位置为1

相关参数为

  • code:10
  • status:1

1的二进制如下

0001

很明显可以看出,只需要将两个数字进行 或(|)运算即可


实现如下


int disable(int code, int status) {
   //将指定位数置为1 即不允许
   return code | status;
 }
    

enable

enable函数的意义是:将指定位置为0。

要求:

  • 随便一个状态数字在enable之后进行is返回true
  • 对状态数字进行反复enable,结果都不能有变化(意思是不能简单的使用取反操作)

方法申明如下

int enable(int code, BitInfo status);

在此将10的最低位置为1

相关参数为

  • code:10
  • status:1

在disable函数中,已经将指定位置为1了,那么只需要把这个1再给变回来即可,且不能影响其他位

因此只需要将10和2先后进行运算和异或运算即可


方法实现如下:

int enable(int code, BitInfo status) {

    //将指定位数置为0 即允许
    //将位和1 或,则指定位一定为1,再与status异或,指定位则为0

    return (code | status.code()) ^ status.code();
}

使用

那么在使用上也很简单。

例如我要判断 状态 254 是否可以接收短信,只需要这样调用


BitInfo.is(254,UserStatus.RECEIVE_SMS)


返回true即代表可以接收短信

总结

使用位运算存储状态有以下优点:

  • 节约空间
  • 易于扩展
  • 可以同时存储复合状态

但是缺点也很明显:

  • 不够直观,必须通过程序才能看出具体状态含义
  • 逻辑复杂,过于专注底层位运算可能会搞混,但是只关注抽象层就好得多

上述代码中还是有一定问题的。例如同时enable多个状态,调用上就比较复杂,会出现很多括号;这可以通过链式调用来解决,我在此就不作修改了。

展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部