位掩码:一种非常深奥(且不实用)的布尔值管理方式
位运算符
对一个简单的问题进行过度设计
位掩码
获取值
切换值
现在一起
结论:我为什么要费心进行位掩码呢?
2024年7月20日:六年过去了,我拿到了计算机科学学位,也改变了本文中大部分负面观点。请参阅下方嵌入的文章,了解我更新后的想法,这是一篇关于位掩码的赞赏文章。
你有没有想过位运算符是做什么用的?像 JavaScript 这样高级的语言为什么需要这种底层运算符?首先,它在 JavaScript 中确实有它的用武之地。只是大多数用法并不像其他运算符那样显而易见。事实上,除非你眯着眼睛仔细观察电脑屏幕,否则大多数用法根本就看不出来。相信我,我试过了。我可不是在开玩笑。在我相对较短的 JavaScript 使用经验中(截至撰写本文时为三年),我很少在日常情况下看到位运算符的身影。也许是我观察得不够深入,但我认为个中缘由显而易见。读完本文,你就会明白其中的原因。
位运算符
注:我并不要求您对二进制数系统和位运算符有深入的了解,但我假设您至少对它们有所了解。如果您不熟悉,我强烈建议您在继续阅读本文之前先查阅一些相关资料(明白我的意思吗?) 。
位运算符允许我们操作组成二进制数的各个位。为了方便快速回顾,这里列出了一个常用位运算符的功能表。
// I will use the binary notation prefix ("0b") a lot in this article.
const num1 = 0b1010; // 10
const num2 = 0b1111; // 15
// NOT num1
~num1; // 0b0101 (complement) === -11
// num1 AND num2
num1 & num2; // 0b1010 === 10
// num1 OR num2
num1 | num2; // 0b1111 === 15
// num1 XOR num2
num1 ^ num2; // 0b0101 === 5
// Bit-shift to the left by 1
num1 << 1; // 0b10100 === 20
// Bit-shift to the right by 1
num >> 1; // 0b0101 === 5
我的意思是,每天学习新知识固然很好,但你什么时候才能用到这些知识呢?位运算符有什么实际应用吗?简而言之,没有。虽然它在代码压缩、内存优化和其他一些应用场景中可能有用,但使用位运算符会降低代码的可读性。因为你需要将“十进制模式”的思维切换到“二进制模式”,所以代码读起来会更加晦涩难懂。不过,这并不能阻止我们学习,对吧?我们来这里都是为了学习。所以,废话不多说,下面就来介绍位掩码。
对一个简单的问题进行过度设计
说实话,我很难给“位掩码”下一个简单的定义。在我看来,它简直是个怪胎。对我来说,位掩码可以理解为一种查询。使用位掩码意味着查询某个二进制数中的位。如果你对这个定义感到困惑,我完全理解。我必须承认,这并非一个完美的定义。如果你能想到更好的定义,请在下方留言。我非常乐意更新这篇文章,将其纳入你的定义。
总之,没有互补的例子,定义就毫无意义。假设我们有一个对象,它存储着与应用程序中找到的配置相对应的布尔值。
// Mock app settings
const config = {
isOnline: true,
isFullscreen: false,
hasAudio: true,
hasPremiumAccount: false,
canSendTelemetry: true
};
至此,我们的工作就完成了。我们可以直接将其存储在 JSON 文件中。这是最简单的实现方式。然而,我们可以使用位掩码来“过度设计”这个问题。在 JavaScript 中,可以通过将数字类型传递给函数,将其显式地转换(或强制)为布尔值Boolean。请注意,在这种情况下,` Booleanis` 并非用作构造函数。它只是将数字类型(或任何类型)转换为其等效的布尔“真值”的一种手段。例如:
Boolean(-2); // true
Boolean(-1); // true
Boolean(0); // false
Boolean(1); // true
Boolean(2); // true
Boolean(Math.PI); // true
Boolean(Number.MAX_SAFE_INTEGER); // true
由于0它本身并不是一个“真值”,所以它的值为 false false。这种关系启发我们思考如何将多个布尔值转换为一个数字。我们可以将应用程序设置存储为一个数字,而不是一个对象。没错,你没听错,或者说你没看错。首先,我们将布尔值视为1s 和0s,其中 s1是 true , trues 是0false false。这些1s 和s从左到右分别0对应对象中的每个属性。config
// For reference only
const config = {
isOnline: true,
isFullscreen: false,
hasAudio: true,
hasPremiumAccount: false,
canSendTelemetry: true
};
// isOnline: 1
// isFullScreen: 0
// hasAudio: 1
// hasPremiumAccount: 0
// canSendTelemetry: 1
// Thus, we have the binary number 0b10101.
let configNumber = 0b10101; // 21
位掩码
注意:接下来是文章中最奇特的部分。我要开始讲一些玄妙的内容了。希望你已经充分锻炼了大脑,因为从现在开始,你将面临一场高强度的脑力训练。某些章节可以反复阅读。毫不夸张地说,这是一个相当棘手的话题。
现在我们已经将整个对象简化成一个数字,就可以对它使用位运算符了。你可能会问,为什么要这样做呢?嗯,这就是位掩码的本质。
位掩码是一种“选择”所需位的方法。选择特定位时,该位始终是 2 的幂,因为任何 2 的幂都对应于被“置位”的该特定位。由于左移位本质上是乘以 2(类似于 2 的幂),因此您可以将左移位视为“选择”所需位的一种方式。
// Selecting the 1st bit from the right
// 2 ** 0
// 1 << 0
0b00001 === 1;
// Selecting the 2nd bit from the right
// 2 ** 1
// 1 << 1
0b00010 === 2;
// Selecting the 3rd bit from the right
// 2 ** 2
// 1 << 2
0b00100 === 4;
// Selecting the 4th bit from the right
// 2 ** 3
// 1 << 3
0b01000 === 8;
// Selecting the 5th bit from the right
// 2 ** 4
// 1 << 4
0b10000 === 16;
如果我们想选择多个比特,也可以这样做。
// Selecting the 1st and 5th bit from the right
0b10001 === 17;
// Selecting the 3rd and 4th bit from the right
0b01100 === 12;
// Selecting the 2nd, 4th, and 5th bit from the right
0b11010 === 26;
// Selecting the 1st, 2nd, and 4th bit from the right
0b01011 === 11;
// Selecting ALL the bits
0b11111 === 31;
获取值
位掩码允许我们提取数字中单个比特的值configNumber。我们该如何操作呢?假设我们想要获取某个值hasAudio。我们知道该hasAudio属性位于从右侧数起的第三位configNumber。
let configNumber = 0b10101; // 21
// Shifting 0b1 to the left 2 times gives the 3rd bit from the right
const bitMask = 0b1 << 2; // 4
// Since we know that the 3rd bit from the right corresponds to the hasAudio property...
const query = configNumber & bitMask; // 4
// ...we can test its "truthiness" by using the AND operator.
const truthiness = Boolean(query); // true
// The truthiness IS the value we want to extract.
truthiness === config.hasAudio; // true
此时,你可能会问……
“就算它回来了又怎样
4?
”“就算它是被迫回来的又怎样true?”
“就算它是‘说真话’的又怎样?”
如果你问的是这个问题,那么你已经自己回答了。该属性4已被强制转换为。这正是原始对象中true该属性的精确值。我们已经通过位掩码成功提取了该属性的值。hasAudioconfighasAudio
那么,如果我们尝试查询一个“假”属性(例如 `is`)会发生什么呢isFullscreen?位掩码会反映原始对象中的相同值吗config?事实上,它会。我们知道该isFullScreen属性位于 `is` 对象中从右数第四位configNumber。
let configNumber = 0b10101; // 21
// Shifting 0b1 to the left 3 times gives the 4th bit from the right
const bitMask = 0b1 << 3; // 8
// Since we know that the 4th bit from the right corresponds to the isFullscreen property...
const query = configNumber & bitMask; // 0
// ...we can test its "truthiness" by using the AND operator.
const truthiness = Boolean(query); // false
// The truthiness IS the value we want to extract.
truthiness === config.isFullscreen; // true
我们还可以更疯狂地选择多个位bitMask,但我把这留给你们思考。
你可能已经注意到其中的规律了。位运算符的结果AND决定了truthinessa 的值query。这个truthiness值本质上就是我们最初想要获取的属性的实际值。是的,我知道,这听起来很神秘。我当时也是同样的反应。它太巧妙了,我当时无法完全理解。
既然我们知道如何从特定位中提取布尔值,那么我们如何操作位呢?
切换值
当我们想要切换位时,遵循同样的逻辑。我们仍然使用位掩码来选择我们感兴趣的位,但是我们使用XOR按位运算符 ( ^) 而不是AND按位运算符 ( &) 来进行切换query。
假设我们要切换某个canSendTelemetry属性。我们知道它位于从右边数起的第一个位置。
let configNumber = 0b10101; // 21
// Shifting 0b1 to the left 0 times gives the 1st bit from the right,
// which corresponds to the canSendTelemetry property
const bitMask = 0b1 << 0; // 1
// Toggling the 1st bit from the right
const query = configNumber ^ bitMask; // 20
// Setting the query as the new configNumber
configNumber = query;
canSendTelemetry现在,如果我们尝试从新的值中提取该属性configNumber,我们会发现它不再设置为true。我们已经成功地将该位从 切换true到false(或者更确切地说,从 切换1到0)。
现在一起
重复这项工作确实很繁琐。既然我们都想节省一些击键次数,那就让我们创建一些实用函数来帮我们完成这些工作吧。首先,我们将编写两个实用函数来提取一个比特的“真值”:一个函数根据给定的比特掩码提取其“真值”,另一个函数根据待提取比特从右到左的索引位置提取其“真值”。
/**
* Extracts the "truthiness" of a bit given a mask
* @param {number} binaryNum - The number to query from
* @param {number} mask - This is the bitmask that selects the bit
* @returns {boolean} - "Truthiness" of the bit we're interested in
*/
function getBits(binaryNum, mask) {
const query = binaryNum & mask;
return Boolean(query);
}
/**
* Extracts the "truthiness" of a bit given a position
* @param {number} binaryNum - The number to query from
* @param {number} position - This is the zero-indexed position of the bit from the right
* @returns {boolean} - "Truthiness" of the bit we're interested in
*/
function getBitsFrom(binaryNum, position) {
// Bit-shifts according to zero-indexed position
const mask = 1 << position;
const query = binaryNum & mask;
return Boolean(query);
}
最后,我们来编写一个用于切换一个或多个位的实用函数。该函数返回binaryNum切换所选位后得到的新值。
/**
* Returns the new number as a result of toggling the selected bits
* @param {number} binaryNum - The number to query from
* @param {number} mask - This is the bitmask that selects the bits to be toggled
* @returns {number} - New number as a result of toggling the selected bits
*/
function toggleBits(binaryNum, mask) {
return binaryNum ^ mask;
}
现在我们可以将这些实用函数应用于前面的示例中。
const config = {
isOnline: true,
isFullscreen: false,
hasAudio: true,
hasPremiumAccount: false,
canSendTelemetry: true
};
let configNumber = 0b10101;
// Extracts hasPremiumAccount
getBits(configNumber, 1 << 1); // false
getBitsFrom(configNumber, 1); // false
// Toggles isOnline and isFullscreen
toggleBits(configNumber, (1 << 4) + (1 << 3)); // 0b01101 === 13
结论:我为什么要费心进行位掩码呢?
这是一个很好的问题。坦白说,我并不建议经常使用这种方法,甚至最好不要使用。尽管它很巧妙,但对于日常使用来说过于深奥。大多数情况下,它既不实用又难以阅读。需要不断地查阅文档并保持警惕,才能确保选择和操作正确的部分。总的来说,这种方法的应用场景并不多,尤其是在像 JavaScript 这样的高级语言中。但是,如果确实需要,这不应该阻止你使用它。作为程序员,我们的职责是确定哪些算法对用户(易用性)和开发者(可维护性)都是最佳选择。
如果真是这样,那我写一整篇文章来讨论这个问题又有什么意义呢?
- 这篇文章是写给那些资深计算机科学家的。他们将从中受益匪浅,尤其是那些刚刚开始深入探索计算机科学这个奇妙世界的人。更广泛地说,你不必是计算机科学家也能从这篇文章中获益。任何对这类话题感兴趣的人都会发现这些位掩码相关的复杂概念的价值所在。
- 对于那些不熟悉计算机科学的人来说,现在你们掌握了更多工具。如果将来需要,你们也可以使用位掩码。我希望这篇文章能鼓励你们进行创造性思考。过度设计是我们最终都会遇到的难题。不过,它并非完全是坏事。过度设计只是对(过于)创造性思考的一种负面诠释。我们的大脑倾向于探索各种想法,即使这些想法并不实际。当然,为了提高效率,我们必须避免过度探索,但偶尔进行一些探索总是有益的。让你的大脑运转起来,它就会为你服务。
- 至于我,写这篇文章是为了检验自己。我想知道自己目前为止学到了多少。除此之外,我也乐于教别人。教别人也能学到很多东西。这正是我为这个网站写文章的主要原因。你知道,这样做确实有好处。如果你还没试过,不妨去教别人一些新东西。你可能会惊讶地发现,这也能给你带来多大的帮助。
请负责任地使用位掩码。
文章来源:https://dev.to/somedood/bitmasks-a-very-esoteric-and-impractical-way-of-managing-booleans-1hlf