位运算保存状态

寒江蓑笠翁大约 9 分钟技术日志go位运算go

位运算保存状态


理论

在go里面,没有提供枚举这一类型,所以我们会通过声明常量来表示一些特定的状态,比如下面这种形式

const (
	A = 1 + iota
	B
	C
	D
)

var status uint8

通过const + iota的方式来定义一些状态,这样做的缺点在于,一个变量只能同时存储一个状态,如果要同时表示多个状态,就需要使用多个变量,而使用位来存储这些状态可以很好的解决这种问题,其过程只涉及到了简单的位运算。

比特位存储状态原理是每一个比特位表示一个状态,1表示拥有此状态,0表示未拥有此状态,那么总共能表示多少个状态取决于有多少个比特位,在go语言中,使用uint64类型可以最多可以表示64个状态。在这种情况下,其所存储状态的值就有一定的要求,其值必须是2的整数次方,比如2的2次方

0b10

2的8次方

0b10000000

假设现在用一个无符号8位整数来存储这些状态,意味着可以有8个比特位可以使用,也就是uint8(0)

const (
	A = 0b1 << iota
	B
	C
	D
)

var status uint8

将其与0b10进行或运算,或运算的符号是|

    00000010
 |  00000000
------------
    00000010

或运算的规则是同为0取0,否则取1,进行或运算后,就可以将该状态的标志位记录到变量中。同理,也可以存储多个其它不同的状态,将上面计算的结果与0b10000000再次进行或运算后,此时状态变量的二进制位中,已经有两个比特位为1。

    10000000
 |  00000010
------------
    10000010

如果要一次性存储多个状态,可以先将几个状态进行或运算,再存储到状态变量中,比如一次性存储状态ABCD

    00000001
 |  00000010
------------
    00000011
 |  00000100
------------
    00000111
 |  00001000
------------
    00001111
 |  00000000
------------
    00001111

最终status的值就是

0b00001111

既然有存储状态,就肯定要读取状态,读取状态的原理同样十分简单。假如要确认状态A是否存在于status变量中,使用与运算&即可,其规则为同为1取1,否则取0,由于这些状态值全都是2的正整数次方,二进制位中永远只有一个位为1,所以两者进行与运算时,只有相同的那一个比特位才能为1,其余全为0,如果计算结果为0,说明指定位不相同,则不包含此状态,计算过程如下。

    00000001
 &  00001111
------------
    00000001

同理,如果想判断多个状态是否存在于status中,将多个状态值进行或运算,然后将结果与status进行与运算即可,比如下面判断是否同时包含状态ABC。

    00000001
 |  00000000
------------
    00000001
 |  00000010
------------
    00000011
 |  00000100
------------
    00000111
 &  00001111
------------
    00000111

最后一个操作就是撤销状态,将指定状态从status中删除,经过上面两个操作的讲解后相信可以很容易就能想到删除的原理。实际上有两种方法可以操作,其结果都是一样的,第一种是将指定状态取反,然后将结果与status相与,就能得到删除指定状态后的status。假设删除状态D其过程如下,

 ~  00001000
------------
    11110111
 &  00001111
------------
    00000111

取反会将自身的每一个比特位反转,反转后只有一个比特位为0,也就是要删除的比特位,这样一来将与status进行与运算,就能将指定比特位置0。另一个方法就是直接将两者进行异或运算,异或的规则是不相同取1,相同取0,计算过程如下

    00001000
 ^  00001111
------------
    00000111

可以看得出来异或就等于取反后相与,两者是完全等价的。如果要删除多个状态,跟之前同理,多个状态进行或运算后再进行异或,比如下面删除状态ABC

    00000001
 |  00000000
------------
    00000001
 |  00000010
------------
    00000011
 |  00000100
------------
    00000111
 ^  00001111
------------
    00001000

实现

理论部分讲完过后,下面看看怎么用代码来进行实现,这种操作是不限语言的,这里使用go语言来进行实现。需要注意的是,go语言中取反运算符和异或运算符是同一个,都是^符号。

type BitFlag uint64

首先可以声明一个BitFlag类型,其底层类型为uint64,最多可以同时存储64个状态,在实际代码中可以直接使用位运算来进行操作,这里选择稍微封装了一下。

type BitFlag uint64

func (bf *BitFlag) Store(flags ...uint64) {
	for _, flag := range flags {
		*bf |= BitFlag(flag)
	}
}

func (bf *BitFlag) Check(flags ...uint64) bool {
	var f uint64
	for _, flag := range flags {
		f |= flag
	}

	return *bf&BitFlag(f) != 0
}

func (bf *BitFlag) Revoke(flags ...uint64) {
	var f uint64
	for _, flag := range flags {
		f |= flag
	}
	*bf ^= BitFlag(f)
}

可以看到代码量非常少,实现起来也很简单,下面是一个简单的使用案例

const (
	A = 0b1 << iota
	B
	C
	D
	E
	F
	G
)

func main() {
	var flag BitFlag
	flag.Store(A, B, C, D, E)
	fmt.Println(flag.Check(A, E))
	fmt.Println(flag.Check(F, G))
	flag.Revoke(A, D)
	fmt.Println(flag.Check(A, D))
}

输出

true
false
false
上次编辑于:
贡献者: 246859