十六进制状态和标签管理

1,442 阅读8分钟

前言

在业务开发中,我们时常面临关于状态的处理,也经常看见一些不怎么有效率的处理方式,例如将各种状态值都存储为字符串。字符串的处理效率远远不及数字的处理效率,所以性能上较好的方法是将状态值定义为整型数字,在业务上再与具体含义做好映射关系。

本以为用整型定义状态值已经是最佳实践了。但偶然间看见的一篇文章(就算不去火星种土豆,也请务必掌握的 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

如果采用十进制表示状态,那么取值应该为20,21,22,23,...,2n2^0, 2^1,2^2,2^3,...,2^n

在代码编写上,一方面为了便于表示,另一方面需要与通常的十进制数字计算加以区分,实践中往往采用十六进制进行表示(左边为十六进制,右边为二进制、十进制):

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,,提升了效率,也降低了存储。

十六进制多标签管理

在数据库中,我们经常会面临"一对多"关系多端的处理问题。要么将多端独立存表,每次查询多一次表关联;要么将多端存储在同一张表中的一个字段里(例如存为stringjson字段),给字段上的查询带来了诸多困难和低效。例如,字段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字符串匹配的效率高得多。

总结

  • 在面临多状态的处理、多标签的处理时,不妨试试十六进制表示法。

  • 十六进制表示法,使用位运算,操作效率高,存储空间也低。