位运算实现枚举
计算机底层是使用二进制存储数据的,对于直接计算二进制的操作都可以称之为位运算。本文会使用 Go 语言来了解位运算,并且通过一个例子来举例位运算是怎么实现枚举的,并且通过 Go 的一个框架:GoFrame 来看看实际的应用。
位运算基础语法
这里以 Go 语言举例子,其他语言大同小异。位运算基本规则:
p | q | p & q | p | q | p^q |
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
示例:
// 与运算,两个位都为1时,结果才为1
fmt.Printf("%b\n", 0b110&0b100) // 100
// 或运算,两个位都为0时,结果才为0
fmt.Printf("%b\n", 0b110|0b100) // 110
// 异或运算,两个位相同为0,相异为1
fmt.Printf("%b\n", 0b110^0b100) // 010
// 左移运算,全部左移若干位,高位丢弃,低位补0
fmt.Printf("%b\n", 0b110<<1) // 1100
// 右移运算,全部右移若干位,高位丢弃,低位补0
fmt.Printf("%b\n", 0b110>>1) // 11
基础应用
判断奇偶数
在二进制中,末位值如果是1,那么这个数必然是奇数,0必然为偶数,利用这一点,可以使用与 1 位运算,来判断一个数的奇偶性:
i & 1 = 1 // 奇数
i & 1 = 0 // 偶数
fmt.Println(11 & 1) // 输出1
fmt.Println(10 & 1) // 输出0
2 的次方运算
在左右移运算中,如果一个数m左移了n位,最后的结果就等于这个数乘以 2^n(^代表次方运算),右移则正好相反,等于除以 2^n:
fmt.Printf("%d\n", 1<<16) // 1 * 2^16 = 65536
fmt.Printf("%d\n", 2<<16) // 2 * 2^16 = 131072
fmt.Println(65536 >> 8) // 65536 / 2^8 = 256
异或特性
两个相同的数异或结果为 0,一个数与 0 异或为他本身:
i ^ i = 0
i ^ 0 = i
实现枚举
假如现在某视频网的 UP 主发布了一个视频,用户可以对该视频点赞、收藏、评论、分享四种操作。最直观的做法是在程序中会创建四个变量:hasThumb,hasStore,hasCmt,hasShare,当赋值为 true 时代表已经完成这种操作,反之代表未完成。
能不能换一种思路来设计呢?
经过一番思考后,我们发现可以使用一个字符串同时保存四种状态,比如 1101,1001
。这个字符串的某一位上的 1 就代表已经完成了某种操作,0 就代表未完成。那么 1101
这个字符串就可以代表已点赞,未收藏,已评论、已分享(反过来的)。这个字符串每一位上只会保存 0 和 1,那么它或许应该表示为二进制数更为合适。在大多数的编程语言中,使用 0b
开头表示这个数是二进制的,0b1101
就可以保存四种不同的状态。
思想已经有了,那么在实际的程序上我们还需要对 0bxxxx
的每一位做出相应的赋值与判断,该怎么操作呢?这个时候就可以请出我们的主角:位运算。在使用了位运算后,就会发现这一切都是那么的顺其自然。
首先我们定义点赞、收藏、评论、分享四种操作的二进制值:
// 这里采用的是 Go 语法,实际上生成的值放在注释后
const (
hasThumb = 1 << iota // 1<<0 = 0b1
hasStore // 1<<1 = 0b10
hasCmt // 1<<2 = 0b100
hasShare // 1<<3 = 0b1000
)
或许眼尖的里已经发现,这四个值都有且只有一个 1,且它们的 1 错开在不同的位数上,1 就是他们的标记位。那么当它们之间就行位运算就会发生神奇的化学反应:
hasThumbAndStore = hasThumb | hasStore
hasCmtAndShare = hasCmt | hasShare
Printf(hasThumbAndStore) // 11
Printf(hasCmtAndShare) // 1100
Printf(hasThumbAndStore|hasCmtAndShare) // 1111
可以看到,任意的值进行位运算后,结果总是继续保存着它的标记位。假如一个用户进行了点赞、评论、分享操作,那么,就可以对它们进行位或运算:
Printf(hasThumb|hasCmt|hasShare) // 1101
hasThumb
的标记位在第一位,hasCmt
在第三位,hasShare
在第四位,结果的位数和他们保持一致。
保存信息已经完成了,怎么从信息中判断是否已经点赞、评论或者分享呢,这个时候就可以使用位与运算:
hasRes = hasThumb | hasCmt | hasShare
Printf(hasRes&hasThumb == hasThumb) // true
Printf(hasRes&hasStore == hasStore) // false
Printf(hasRes&hasCmt == hasCmt) // true
Printf(hasRes&hasShare == hasShare) // true
发现了吗,一切就是这么顺其自然。
如果这个时候用户取消了点赞,该应该怎么做呢,我们已经使用了位与和位或,还剩下异或没有使用,我们来看一下使用异或后会发生什么:
hasRes = hasThumb | hasCmt | hasShare
Printf(hasRes) // 1101
Printf(hasRes&hasThumb == hasThumb) // true
hasRes = hasRes ^ hasThumb
Printf("%b\n", hasRes) // 1100
Printf(hasRes&hasThumb == hasThumb) // false
可以看到使用 hasRes ^ hasThumb
后,hasThumb
的标记位就从 hasRes
上移除了,这一切就是这么的巧夺天工!
Gf 框架巧用
这里我们通过 Go 开源框架:GoFrame(简称gf) 是怎么巧妙的通过位运算来实现 ORM 链式操作的空值过滤的。
前文介绍
gf 框架的 ORM 链式操作中提供了 OmitEmpty
空值过滤的方法。它用作来过滤 ORM 操作时传入的空值字段,如果链式调用了该方法,则会将传入进来的空字段过滤,不会生成该字段的 SQL 子句。这里通过伪代码对比一下:
Table.Data({
"name" : "john",
"update_time" : "",
}).Where("id", 1).Update()
// 生成的 SQL 语句:
// UPDATE `TableName` SET `name`='john',update_time=null WHERE `id`=1
// 加入 OmitEmpty 方法来过滤掉 update_time 对应的空字符串
Table.Data({
"name" : "john",
"update_time" : "",
}).Where("id", 1).Update()
// 生成的 SQL 语句:
// UPDATE `TableName` SET `name`='john' WHERE `id`=1
OmitEmpty
有两个子方法:OmitEmptyWhere
和 OmitEmptyData
。其中 OmitEmptyWhere
只会对查询语句生效,OmitEmptyData
只会对写入语句生效。而 OmitEmpty
则同时包含两种方法,也就是说 OmitEmptyWhere + OmitEmptyData = OmitEmpty
。
源码解析
这里会将源码翻译成伪代码,有兴趣的可以访问 https://github.com/gogf/gf 查看更为详细的信息。
在 gf 框架中,存在一个 Model 结构体,其中有一个字段:option。当链式语句调用 OmitEmpty,OmitEmptyWhere,OmitEmptyData
这三种方法的任意一种时,option 会赋值该字段的特征值,特征值符合以下规律:
- 当链式调用了
OmitEmpty
方法时,无论时查询还是写入语句都要过滤空值; - 当链式调用了
OmitEmptyWhere
方法时,查询语句过滤空值,写入不过滤; - 当链式调用了
OmitEmptyData
方法时,写入语句过滤空值,查询不过滤;
这里如果按照常规做法,可以在调用 OmitEmpty
方法时,赋值 option 为 [Read, Write] 数组;调用 OmitEmptyWhere
方法时,赋值 option 为 [Read];调用 OmitEmptyData
方法时,赋值 option 为 [Write]。最后在组成 SQL 语句时,来判断当前 option 值,决定要不要过滤空值。
这种做法显然不够优雅,那么 gf 框架是怎么实现该需求的呢。gf 框架先是定义了三个常量:
// 实际上的源码不是这样,这里是简化的写法
const (
optionOmitEmptyWhere = 1 // 二进制: 0b1
optionOmitEmptyData = 2 // 二进制: 0b10
optionOmitEmpty = optionOmitEmptyWhere | optionOmitEmptyData // 值应该是 0b1 | 0b10 = 0b11,即十进制3
)
然后在调用空值过滤方法时,会将对应的特征值赋值给 option。在调用 option 的地方这样判断:
option&optionOmitEmptyData > 0 // 如果大于0代表写入数据需要空值过滤
option&optionOmitEmptyWhere > 0 // 如果大于0代表查询数据需要空值过滤
很自然而然,不是吗?
本文目录