二进制位操作对多状态组合的应用

这其实是属于前辈级程序员们玩剩下的东西,去写win32的程序,调用win32 api的时候,经常会碰到属性状态的组合,比如这个例子

int DisplayResourceNAMessageBox()
{
    int msgboxID = MessageBox(
        NULL,
        (LPCWSTR)L"Resource not available\nDo you want to try again?",
        (LPCWSTR)L"Account Details",
        MB_ICONWARNING | MB_CANCELTRYCONTINUE | MB_DEFBUTTON2
    );

    switch (msgboxID)
    {
    case IDCANCEL:
        // TODO: add code
        break;
    case IDTRYAGAIN:
        // TODO: add code
        break;
    case IDCONTINUE:
        // TODO: add code
        break;
    }

    return msgboxID;
}

MessageBox这个函数的第四个参数,用于定义窗体的显示形式,可以是多种形式的组合,就像上面的例子,传入的就是MB_ICONWARNING | MB_CANCELTRYCONTINUE | MB_DEFBUTTON2

这种形式的好处就在于,多个属性组合时,不需要为函数定义一大堆的入参,通过巧妙的属性值选取,配合二进制位操作,无论多少个属性,都可以通过一个参数完成。

假如某函数可接受4种状态的组合:a,b,c,d
我们定义四个常量:

const STATUS_A = 0b1 // 二进制 1
const STATUS_B = 0b10 // 二进制 10
const STATUS_C = 0b100 // 二进制 100
const STATUS_D = 0b1000 // 二进制 1000

为什么这么定义? 因为二进制的或运算(|)有如下特征:

0b1000 | 0b10  // ==> 0b1010
0b10 | 0b101  // ==> 0b111
...

二进制位右对齐后,两个同位置的值有一个为1,则结果为1,否则结果为0。

对于我们的例子:

let status = STATUS_B | STATUS_D // 得到 status 为二进制 1010

很明显STATUS_B对应的第二位与STATUS_D对应的第四位被置为了1,而另两位依然是0

人眼是很容易看明白哪位为0,哪位为1,程序有什么快速的方式知道呢?

答案,依然是位运算,不过这次用与运算(&),特征如下:

0b101 & 0b11  // ==> 0b101
0b111 & 0b10  // ==> 0b110
...

二进制位右对齐后,两个同位置的值同时为1,则结果为1,其中一项不为1,则结果为0。

利用这个特性,当我们需要知道某状态是否被设置时,只需要入参的值与该状态的值做与运算就好了,因为该状态的值对应位的值肯定为1,所以就有:

let status = STATUS_B | STATUS_D // 得到 status 为二进制 1010

console.log(status & STATUS_A) // 输出0
console.log(status & STATUS_B) // 输出十进制2 对应二进制 10
console.log(status & STATUS_C) // 输出0
console.log(status & STATUS_D) // 输出十进制8 对应二进制 1000

聪明的你,应该已经知道答案了吧。

完整示例:

// 采用parseInt中转一下,直接使用二进制定义更直观
const STATUS_A = 0b1
const STATUS_B = 0b10
const STATUS_C = 0b100
const STATUS_D = 0b1000

function statusHandle(statuMask){
    if(statuMask & STATUS_A){
        console.log('状态A被设置')
    }
    if(statuMask & STATUS_B){
        console.log('状态B被设置')
    }
    if(statuMask & STATUS_C){
        console.log('状态C被设置')
    }
    if(statuMask & STATUS_D){
        console.log('状态D被设置')
    }
}
/*      test       */
statusHandle(STATUS_C)
// 状态C被设置
statusHandle(STATUS_A | STATUS_D)
// 状态A被设置
// 状态D被设置