前言
在业务开发中,我们时常面临关于状态的处理,也经常看见一些不怎么有效率的处理方式,例如将各种状态值都存储为字符串。字符串的处理效率远远不及数字的处理效率,所以性能上较好的方法是将状态值定义为整型数字,在业务上再与具体含义做好映射关系。
本以为用整型定义状态值已经是最佳实践了。但偶然间看见的一篇文章(就算不去火星种土豆,也请务必掌握的 Android 状态管理最佳实践),让我对"十六进制状态管理法"印象深刻。本文将再次对十六进制状态的表示进行举例说明,并对为什么要这么表示,都有哪些好处进行说明。
同时,多状态的管理让我联想到之前的开发中,多标签字段的处理让我们尝尽了苦头,要方便没效率,要效率不方便。前后对比联系起来,多标签管理与状态管理有异曲同工之妙,也能使用十六进制的方式进行表示。本文也会进行举例说明。
十六进制状态表示
十六进制状态表示使用位运算。在计算机的处理中,位运算的速度总是大于加减乘除等运算。因此用进制表示状态具有独特的效率优势。
进制表示的示例
假如有个状态变量,具有初始状态
、中间状态
以及最终状态
三种,而每种状态各自具有两个细分内部状态,这三种大的状态可以称为状态组
。采用十六进制定义这6种状态:
const (
STATUS_1 = 0x0001 // 初始状态1
STATUS_2 = 0x0002 // 初始状态2
STATUS_3 = 0x0004 // 中间状态1
STATUS_4 = 0x0008 // 中间状态2
STATUS_5 = 0x0010 // 最终状态1
STATUS_6 = 0x0020 // 最终状态2
)
复制代码
添加状态
当需要添加某个状态时,使用或运算|:
// 状态组:当需要添加状态时,使用或运算|
const (
INIT_STATUS = STATUS_1 | STATUS_2 // 初始状态
MIDDLE_STATUS = STATUS_3 | STATUS_4 // 中间状态
FINAL_STATUS = STATUS_5 | STATUS_6 // 最终状态
)
复制代码
包含状态
当需要判断是否包含某种状态时使用与运算&,结果为0则代表不包含指定状态
// 当需要判断是否包含某种状态时使用与运算&,结果为0则代表不包含指定状态
const (
CONTAINS_STATUS_1 = (INIT_STATUS&STATUS_1 != 0)
CONTAINS_STATUS_3 = (INIT_STATUS&STATUS_3 != 0)
)
复制代码
func main() {
fmt.Println("初始状态包含状态1:", CONTAINS_STATUS_1)
fmt.Println("初始状态包含状态3:", CONTAINS_STATUS_3)
}
// 初始状态包含状态1: true
// 初始状态包含状态3: false
复制代码
排除状态
当需要排除状态时,使用与运算和取反运算&^
// 当需要排除状态时,使用与运算、取反运算&^(注意取反操作在go中为^,其他语言通常为~)
const (
INIT_STATUS_1 = INIT_STATUS & ^STATUS_2 // == STATUS_1
MIDDLE_STATUS_3 = MIDDLE_STATUS & ^STATUS_4 // == STATUS_3
FINAL_STATUS_5 = FINAL_STATUS & ^STATUS_6 // == STATUS_5
)
复制代码
func main() {
fmt.Println(
INIT_STATUS_1 == STATUS_1, // 排除2后等于1
MIDDLE_STATUS_3 == STATUS_3, // 排除4后等于3
FINAL_STATUS_5 == STATUS_5, // 排除6后等于5
)
}
// true true true
复制代码
进制表示的原理
其实状态的进制表示,不一定非要使用十六进制,二进制、八进制、十进制任何一种进制都可以。本质上,是要满足:在进行与或操作时,各种状态值要严格不同。比如下述两个二进制数:
0001
0011
复制代码
不能表示两种不同的状态值。因为0011
包含了0001
。要让状态表示严格不同,则应该让每一个二进制位独占1:
0001 = 2^0 = 1
0010 = 2^1 = 2
0100 = 2^2 = 4
1000 = 2^3 = 8
复制代码
如果采用十进制表示状态,那么取值应该为
在代码编写上,一方面为了便于表示,另一方面需要与通常的十进制数字计算加以区分,实践中往往采用十六进制进行表示(左边为十六进制,右边为二进制、十进制):
0x0001 = 0000000000000001 = 2^0 = 1
0x0002 = 0000000000000010 = 2^1 = 2
0x0004 = 0000000000000100 = 2^2 = 4
0x0008 = 0000000000001000 = 2^3 = 8
0x0010 = 0000000000010000 = 2^4 = 16
0x0020 = 0000000000100000 = 2^5 = 32
......
复制代码
在代码中以0x
开头就可以表示一个十六进制数,通过增加位数就可以很方便的扩大表示范围,从右往左,在每一位上依次用尽1,2,4,8
便可往左进一位,使用起来,非常方便。
添加状态
0x0001 | 0x0002:
0000000000000001
0000000000000010
————————————————
0000000000000011
=0x0003
复制代码
包含状态
0x0003 & 0x0001:
0000000000000011
0000000000000001
————————————————
0000000000000001
=0x0001!=0
复制代码
排除状态
0x0003 & ^0x0001:
^0x0001=^0000000000000001=1111111111111110
0000000000000011
1111111111111110
————————————————
0000000000000010
=0x0002
复制代码
进制表示的好处
十六进制状态表示,尤其适用于多种复杂状态的组合。
假如在正常情况下,存在初始状态、中间状态以及最终状态;在异常情况下只有初始状态和最终状态。
在数据库存储上,通常的处理需要两个字段分别来表示是否情况异常、以及对应的三种不同状态。假如用situation
来表示情况是否异常(1-正常,2-异常),用status
来表示所处状态(1-初始状态,2-中间状态,3-最终状态)。
如果我们要查询正常情况下、处于初始或者最终状态的记录,这个查询需要比较两个字段的值:
SELECT * FROM table WHERE situation=1 AND status IN (1,3);
复制代码
借助状态的进制表示,多个字段可以使用status
一个字段进行存储:
const (
STATUS_NORMAL = 0x0001
STATUS_ABNORMAL = 0x0002
STATUS_INIT = 0x0004
STATUS_MIDDLE = 0x0008
STATUS_FINAL = 0x0010
)
复制代码
正常情况下、处于初始或者最终状态的值为:
(STATUS_NORMAL|STATUS_INIT) 和 (STATUS_NORMAL|STATUS_FINAL)
复制代码
查询语句就可以写为:
SELECT * FROM table WHERE status in (0x0001|0x0004, 0x0001|0x0010);
复制代码
数据库只需要比较一个字段的值。复杂度从 m * n 降至 m + n,,提升了效率,也降低了存储。
十六进制多标签管理
在数据库中,我们经常会面临"一对多"关系多端的处理问题。要么将多端独立存表,每次查询多一次表关联;要么将多端存储在同一张表中的一个字段里(例如存为string
或json
字段),给字段上的查询带来了诸多困难和低效。例如,字段label
可以取值为"赌博、色情、诈骗"三者之中的任意一个或多个。当我们想要查询结果为"赌博"和"诈骗"的违规记录,那么label
字段的取值就会有多种:
SELECT * FROM table WHERE label IN ("赌博、诈骗","诈骗、赌博");
复制代码
当有更多种标签结果时,情况会变得非常糟糕。
假如我们又想查询包含"诈骗"的记录:
SELECT * FROM table WHERE label like "%诈骗%";
复制代码
即使建立了索引,这样的like
查询也将使得索引失效,查询效率极低!!!
如果使用十六进制来表示这三个标签:
const (
GAMBLE = 0x0001
SEX = 0x0002
FRAUD = 0x0004
)
复制代码
那么查询结果为"赌博"和”诈骗“记录的sql
将变为:
SELECT * FROM table WHERE label = 0x0001|0x0004;
复制代码
此时,label字段只需要与一个值进行比较。
查询包含"诈骗"的记录sql
将变为:
SELECT * FROM table WHERE label & 0x0004 !=0;
复制代码
直接的位运算比like
字符串匹配的效率高得多。
总结
-
在面临多状态的处理、多标签的处理时,不妨试试十六进制表示法。
-
十六进制表示法,使用位运算,操作效率高,存储空间也低。