【发布时间】:2010-10-07 15:44:42
【问题描述】:
- 您将按位运算用于什么目的?
- 它们为什么这么方便?
- 有人可以推荐一个非常简单的教程吗?
【问题讨论】:
标签: c# bitwise-operators
【问题讨论】:
标签: c# bitwise-operators
虽然每个人似乎都迷上了标志用例,但这并不是位运算符的唯一应用(尽管可能是最常见的)。此外,C# 是一种足够高级的语言,其他技术可能很少使用,但仍然值得了解它们。以下是我能想到的:
<< 和 >> 运算符可以快速乘以 2 的幂。当然,.NET JIT 优化器可能会为您执行此操作(以及其他语言的任何体面的编译器),但如果您'真的每微秒都在烦恼,你可以写这个来确定。
这些运算符的另一个常见用途是将两个 16 位整数填充到一个 32 位整数中。喜欢:
int Result = (shortIntA << 16 ) | shortIntB;
这对于直接与 Win32 函数交互很常见,有时出于遗留原因使用此技巧。
当然,当您想迷惑没有经验的人时,这些运算符很有用,例如在回答家庭作业问题时。 :)
在任何实际代码中,尽管使用乘法会好得多,因为它具有更好的可读性,并且 JIT 将其优化为 shl 和 shr 指令,因此没有性能损失。
^ 运算符 (XOR) 有很多奇怪的技巧。这实际上是一个非常强大的运算符,因为有以下属性:
A^B == B^AA^B^A == BA^B,那么您就不可能知道A 和B 是什么,但是如果您知道其中之一,则可以计算出另一个。我看到使用这个运算符的几个技巧:
在没有中间变量的情况下交换两个整数变量:
A = A^B // A is now XOR of A and B
B = A^B // B is now the original A
A = A^B // A is now the original B
双向链表,每个项目只有一个额外的变量。这在 C# 中用处不大,但对于每个字节都很重要的嵌入式系统的低级编程可能会派上用场。
这个想法是您跟踪第一项的指针;最后一项的指针;对于您跟踪pointer_to_previous ^ pointer_to_next 的每个项目。这样,您可以从任一端遍历列表,但开销仅为传统链表的一半。下面是遍历的C++代码:
ItemStruct *CurrentItem = FirstItem, *PreviousItem=NULL;
while ( CurrentItem != NULL )
{
// Work with CurrentItem->Data
ItemStruct *NextItem = CurrentItem->XorPointers ^ PreviousItem;
PreviousItem = CurrentItem;
CurrentItem = NextItem;
}
要从末尾遍历,您只需将第一行从FirstItem 更改为LastItem。这是另一个节省内存的地方。
我在 C# 中定期使用 ^ 运算符的另一个地方是当我必须为我的复合类型计算 HashCode 时。喜欢:
class Person
{
string FirstName;
string LastName;
int Age;
public int override GetHashCode()
{
return (FirstName == null ? 0 : FirstName.GetHashCode()) ^
(LastName == null ? 0 : LastName.GetHashCode()) ^
Age.GetHashCode();
}
}
【讨论】:
我在我的应用程序中使用按位运算符来保证安全性。我将在 Enum 中存储不同的级别:
[Flags]
public enum SecurityLevel
{
User = 1, // 0001
SuperUser = 2, // 0010
QuestionAdmin = 4, // 0100
AnswerAdmin = 8 // 1000
}
然后为用户分配他们的级别:
// Set User Permissions to 1010
//
// 0010
// | 1000
// ----
// 1010
User.Permissions = SecurityLevel.SuperUser | SecurityLevel.AnswerAdmin;
然后检查正在执行的操作中的权限:
// Check if the user has the required permission group
//
// 1010
// & 1000
// ----
// 1000
if( (User.Permissions & SecurityLevel.AnswerAdmin) == SecurityLevel.AnswerAdmin )
{
// Allowed
}
【讨论】:
我不知道解决你认为的数独问题有多实用,但让我们假设它是。
假设您想编写一个数独求解器,甚至只是一个简单的程序,它会向您显示棋盘并让您自己解决难题,但要确保移动是合法的。
棋盘本身很可能由一个二维数组表示,例如:
uint [, ] theBoard = new uint[9, 9];
值 0 表示单元格仍然是空的,并且范围 [1u, 9u] 中的值是板上的实际值。
现在假设您想检查某个动作是否合法。显然,您可以通过几个循环来完成,但是位掩码可以让您更快地完成任务。在仅确保遵守规则的简单程序中,这无关紧要,但在求解器中却可以。
您可以维护位掩码数组,其中存储有关已插入每行、每列 a 和每个 3x3 框的数字的信息。
uint [] maskForNumbersSetInRow = new uint[9];
uint [] maskForNumbersSetInCol = new uint[9];
uint [, ] maskForNumbersSetInBox = new uint[3, 3];
从数字到位模式的映射,一个位对应于那个数字集,非常简单
1 -> 00000000 00000000 00000000 00000001
2 -> 00000000 00000000 00000000 00000010
3 -> 00000000 00000000 00000000 00000100
...
9 -> 00000000 00000000 00000001 00000000
在 C# 中,您可以通过这种方式计算位模式(value 是 uint):
uint bitpattern = 1u << (int)(value - 1u);
在1u 上面的行中,对应于位模式00000000 00000000 00000000 00000001 左移了value - 1。如果,例如value == 5,你会得到
00000000 00000000 00000000 00010000
一开始,每一行、每一列和每一框的掩码是0。每次在板上放一些数字时,都会更新掩码,因此设置了新值对应的位。
假设您在第 3 行插入值 5(行和列从 0 开始编号)。第 3 行的掩码存储在 maskForNumbersSetInRow[3] 中。我们还假设在插入之前,第 3 行中已经有数字 {1, 2, 4, 7, 9}。掩码 maskForNumbersSetInRow[3] 中的位模式如下所示:
00000000 00000000 00000001 01001011
bits above correspond to:9 7 4 21
目标是设置与此掩码中的值 5 对应的位。您可以使用按位或运算符 (|) 来完成。首先,您创建一个对应于值 5 的位模式
uint bitpattern = 1u << 4; // 1u << (int)(value - 1u)
然后您使用operator | 设置掩码maskForNumbersSetInRow[3] 中的位
maskForNumbersSetInRow[3] = maskForNumbersSetInRow[3] | bitpattern;
或使用更短的形式
maskForNumbersSetInRow[3] |= bitpattern;
00000000 00000000 00000001 01001011
|
00000000 00000000 00000000 00010000
=
00000000 00000000 00000001 01011011
现在您的掩码表明此行(第 3 行)中有值 {1, 2, 4, 5, 7, 9}。
如果要检查,如果某个值在行中,可以使用operator & 检查掩码中是否设置了相应的位。如果应用于掩码的该运算符的结果和对应于该值的位模式非零,则该值已经在该行中。如果结果为 0,则该值不在该行中。
例如,如果你想检查值 3 是否在行中,你可以这样做:
uint bitpattern = 1u << 2; // 1u << (int)(value - 1u)
bool value3IsInRow = ((maskForNumbersSetInRow[3] & bitpattern) != 0);
00000000 00000000 00000001 01001011 // the mask
|
00000000 00000000 00000000 00000100 // bitpattern for the value 3
=
00000000 00000000 00000000 00000000 // the result is 0. value 3 is not in the row.
以下是在棋盘中设置新值、更新适当的位掩码以及检查移动是否合法的方法。
public void insertNewValue(int row, int col, uint value)
{
if(!isMoveLegal(row, col, value))
throw ...
theBoard[row, col] = value;
uint bitpattern = 1u << (int)(value - 1u);
maskForNumbersSetInRow[row] |= bitpattern;
maskForNumbersSetInCol[col] |= bitpattern;
int boxRowNumber = row / 3;
int boxColNumber = col / 3;
maskForNumbersSetInBox[boxRowNumber, boxColNumber] |= bitpattern;
}
有了面具,你可以像这样检查移动是否合法:
public bool isMoveLegal(int row, int col, uint value)
{
uint bitpattern = 1u << (int)(value - 1u);
int boxRowNumber = row / 3;
int boxColNumber = col / 3;
uint combinedMask = maskForNumbersSetInRow[row] | maskForNumbersSetInCol[col]
| maskForNumbersSetInBox[boxRowNumber, boxColNumber];
return ((theBoard[row, col] == 0) && ((combinedMask & bitpattern) == 0u);
}
【讨论】:
Dozens of bit twiddling examples here
代码在 C 中,但您可以轻松地将其改编为 C#
【讨论】:
如果您需要与硬件通信,则需要在某些时候使用位旋转。
提取像素值的 RGB 值。
很多东西
【讨论】:
【讨论】:
它们可以用于大量不同的应用程序,这是我之前在这里发布的一个问题,它使用按位运算:
Bitwise AND, Bitwise Inclusive OR question, in Java
对于其他示例,请查看(例如)标记的枚举。
在我的示例中,我使用按位运算将二进制数的范围从 -128...127 更改为 0..255(将其表示从有符号更改为无符号)。
这里是 MSN 文章 ->
http://msdn.microsoft.com/en-us/library/6a71f45d%28VS.71%29.aspx
很有用。
而且,虽然这个链接:
非常技术性,它涵盖了所有内容。
HTH
【讨论】:
只要您有 1 个或多个项目组合的选项,那么按位通常很容易解决。
一些示例包括安全位(等待贾斯汀的样本..)、安排日期等。
【讨论】:
我不得不说最常见的用途之一是修改位域来压缩数据。您通常会在试图节省数据包的程序中看到这一点。
【讨论】:
我在 C# 中使用它们的最常见的事情之一是生成哈希码。有一些使用它们的相当好的散列方法。例如。对于具有 X 和 Y 的坐标类,我可能会使用这两个整数:
public override int GetHashCode()
{
return x ^ ((y << 16) | y >> 16);
}
这会快速生成一个在由相等对象生成时保证相等的数字(假设相等意味着 X 和 Y 参数在比较的两个对象中相同),同时也不会为低值对象产生冲突模式(可能在大多数应用程序中最常见)。
另一个是组合标志枚举。例如。 RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase
当您针对 .NET 之类的框架进行编码时,通常不需要一些低级操作(例如,在 C# 中,我不需要编写代码来将 UTF-8 转换为 UTF-16,它就在那里在框架中对我来说),但当然有人必须编写该代码。
有一些位旋转技术,例如四舍五入到最接近的二进制数(例如,将 1010 向上舍入到 10000):
unchecked
{
--x;
x |= (x >> 1);
x |= (x >> 2);
x |= (x >> 4);
x |= (x >> 8);
x |= (x >> 16);
return ++x;
}
当你需要它们时它们很有用,但这种情况并不常见。
最后,您还可以使用它们对数学进行微优化,例如 << 1 而不是 * 2,但我只是说这通常是一个坏主意,因为它隐藏了真实代码的意图,几乎没有节省任何东西在性能方面,并且可以隐藏一些细微的错误。
【讨论】:
二进制排序。存在使用除法运算符而不是位移运算符的实现问题。这导致 BS 在集合大小超过 10,000,000 后失败
【讨论】:
您会出于各种原因使用它们:
我相信你能想到其他人。
话虽如此,有时您需要问自己:内存和性能提升是否值得付出努力。写完那种代码后,让它休息一会儿再回来。如果遇到困难,请使用更易于维护的代码重写。
另一方面,有时使用按位运算确实很有意义(想想密码学)。
更好的是,让其他人阅读并广泛记录。
【讨论】:
游戏!
过去,我用它来代表黑白棋选手的棋子。它是 8X8,所以我需要一个 long 类型,然后,例如,如果你想知道所有棋子在哪里 - 你 or 两个玩家棋子。
如果你想要一个玩家的所有可能的步骤,对右边说 - 你>>玩家的棋子表示为一个,然后AND它与对手的棋子一起检查现在是否有共同的1(这意味着有一个对手一块在你的右边)。然后你继续这样做。如果您回到自己的作品中-别动。如果您找到一个清晰的位置 - 您可以移动到那里,并在途中捕获所有碎片。
(这种技术被广泛使用,在包括国际象棋在内的多种棋盘游戏中)
【讨论】: