前言
最近做业务需求有这么一个场景,当某一天的历史净值有对应的分红计划和复权净值调整时,需要算出这一天的净值差和累计净值,将结果追加到这一天的历史净值表中,完成后要将这一天的分红计划和复权净值调整标记为已执行
(在数据库中字段存储值:1 表示已执行;0 表示未执行),也就是说这是一个原子操作,要么都成功,要么都失败。如果失败了,在下一次定时到来将被再次计算。但测试发现偶发情况下,某天的历史净值记录有追加的净值差和累计净值字段,但这天对应的分红计划和复权净值调整记录中,却不是同时被标记了已执行,即分红计划和复权净值调整这两者的标记,可能其中一个是 1 一个是 0。WTF? 当时因为有其他优先级更高的事要处理这个问题暂放,过了一个多小时再回来检查数据发现又正常了,WHAT? 这是什么鬼,什么也没做它自己又好了,但我不能当做什么也没发生,删掉数据重新初始化一遍,果然问题又出现了。这是个问题,必须得找出原因,仔细检查了业务代码逻辑,毫无破绽,那还会是什么原因呢?思来想去,我认为最大的可能性就是程序初始化时,由于同时请求了多个三方接口拿数据,拿到数据后做了不同的业务导致在入库的顺序无法保证,而这些业务又可能会查已入库的数据,查到时和没查到时的业务有些区别,这就很可能会导致数据被覆盖了。也就是说原本是正确的逻辑,正确的数值入库了,但马上又被其他同时在进行的业务覆盖了,结果数据库中看到的就是逻辑不正确的数据。OK,既然有了方向那就改呗,一顿操作后改完更新的开发环境,删掉数据再初始化一遍,然后检查刚刚有问题的数据已经正常了。这条数据是正常了,其他数据是否正常也不知道,那就查一下数据呗。筛选出来的目标数据要符合执行标记一边是 1 一边是 0 的,这个逻辑不正是异或
嘛!问题来了,异或在程序中怎么运算,下面进入正文。
1. 逻辑运算符
在进行业务编码时经常会用到&&(与)
、||(或)
、!(非)
三种逻辑运算,而^(异或)
、同或
、~(按位取反)
比较少会用到。
我们常用 1(真) 和 0(假) 来表示逻辑,这些逻辑运算结果如下:
与:两个输入全为真时输出为真,否则为假。在计算机中使用通常使用&&
表示。
输入A | 输入B | 结果 |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
Java 中有&&
和&
两种,它们都表示与运算,运算逻辑也相同。区别是&&
只能用于逻辑运算,并且会短路逻辑,即当有多个输入时,从左向右依次判断真假,如果某个输入为假则立刻输出假,不再继续判断;而&
既可用于逻辑运算,也可用于位运算,当用于逻辑运算时不会短路逻辑,即当判断到假时,仍会继续向右判断。一般多使用&&
,在某些特殊的场合可能会用到&
。
Go 中有&&
和&
两种,它们都表示与运算,但它们表示的意义和 Java 中有些不同。&&
只能用于逻辑运算,&
只能用于位运算。
Python 中有and
和&
两种,它们都表示与运算,两者都能用于逻辑运算和位运算。同样的and
会短路逻辑&
不会。因为 Python 是弱类型语言,即使函数返回值定义为int
也能return Ture
,因为在 Python 中Ture==1
。
或:两个输入中至少有一个为真时结果为真,否则为假。在计算机中使用通常使用||
表示。
输入A | 输入B | 结果 |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 1 |
Java 中有||
和|
两种,它们都表示或运算,运算逻辑也相同。区别是||
只能用于逻辑运算,并且会短路逻辑,即当有多个输入时,从左向右依次判断真假,如果某个输入为真则立刻输出真,不再继续判断;而|
既可用于逻辑运算,也可用于位运算,当用于逻辑运算时不会短路逻辑,即当判断到真时,仍会继续向右判断。一般多使用||
,在某些特殊的场合可能会用到|
。
Go 中有||
和|
两种,它们都表示或运算,但它们表示的意义和 Java 中有些不同。||
只能用于逻辑运算,|
只能用于位运算。
Python 中有or
和|
两种,它们都表示或运算,两者都能用于逻辑运算和位运算。同样的or
会短路逻辑|
不会。因为 Python 是弱类型语言,即使函数返回值定义为int
也能return False
,因为在 Python 中False==0
。
非:单个输入如果为真则输出假,反之亦反。在计算机中使用通常使用!
表示。
输入 | 结果 |
---|---|
0 | 1 |
1 | 0 |
Java 和 Go 的用法是一致的,且只能用于逻辑运算。Python 则稍有不同,它的非用not
表示,且都能用于逻辑运算和位运算。
2. 位运算符
当使用位运算时,实际是将原始数值转换为二进制按位运算。
异或:两个输入不同时输出真,相同则输出假。在计算机中使用通常使用^
表示。
输入A | 输入B | 结果 |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 0 |
Java、Python 和 Go 的用法都是一致的。
同或:两个输入相同时输出真,不同则输出假。
输入A | 输入B | 结果 |
---|---|---|
0 | 0 | 1 |
1 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
同或运算没有专门的运算符,但我们可以使用a^b^1
来达到同或的运算逻辑。其实同或就是异或取反,但因为这是位运算,所以不能使用逻辑运算符。
按位取反:对二进制的每一位(包括符号位)取反。在计算机中使用通常使用~
表示。
我们可以简单的理解,~x=-(x+1)。即,~5=-(5+1)=-6,~-5=-(-5+1)=4。细节这里先按下不表,在下文中详细介绍。
3. 计算机与二进制
计算机中常会用到二进制(0~1)、八进制(0~7)和十六进制(0~F),其中 10~15 分别用 A~F 表示:
二进制(Binary),例:0011。
八进制(Octal),使用0开头,例:073,转换为二进制是 00111011。
十进制(Decimal),除 +/- 符号外,使用1~9开头,例:98,转换为二进制是 01100010。
十六进制(Hexadecimal),使用0x或0X开头,例:0xFA,转换为二进制是 11111010。
计算机中存储的是二进制的补码。例如,-5 转换为二进制是 1101。最高位是符号位,0表示+,1表示-。
原码:1101
反码:1010
补码:1011
负数的反码是原码除符号位外取反的结果,补码则是在反码的基础上加一。
正数的反码、补码均和原码相同。
现在说说按位取反~
操作。
计算 ~4:
- 将十进制转换为二进制是 0000 0100,因为正数的原码、反码、补码相同,因此计算机中存储的补码也是 0000 0100
- 按位取反
~
是 1111 1011 - 由于计算机存的是补码,因此需要将上一步转换为补码,但需要转换为反码。又因为 1111 1011 的最高位是1,计算机认为它是负数,则按负数的取反规则,符号位不变其余位取反是 1000 0100,补码是 1000 0101
转换为十进制是 -5
计算 ~-5:
- 将十进制转换为二进制是 1000 0101
- 因为计算机中存的是补码,因此需要转换为补码,但需要先转换为反码。则反码是 1111 1010,补码是 1111 1011
- 按位取反
~
是 0000 0100
转换为十进制是 4
4. 结语
知道异或的符号后,赶紧写了几行 py 代码查了下数据库,所有数据都正确,OK 可以发版了。
不得不说 Python 在这种需要简单快速验证的场合挺方便的,没有数据类型限制,强类型的语言还需要写一堆类型转换,当然强类型也有它优势的地方,得灵活变通,在合适的场合选择合适的方式解决问题。