为什么这个Bug如此狡猾?

直觉背叛:我们直觉上认为 -A 就是 “负的A”,但在位运算中它变成了”A的补码”
测试遗漏:我们测试了正数情况,但对负数的测试不够充分
认知偏差:我们都”知道”补码,但真正用到时却忘了它的存在

你发现下面代码中的问题了吗 ?

1
2
3
4
5
6
7
8
9
// 伪代码:用负数表示反选,正数表示正常选择
bool ShouldSelect(int value, int mask)
{
if (-value & mask == Mathf.Abs(-value))
{
return -value > 0;
}
return false;
}

在C#中,A & B-A & B 的区别主要在于对负数处理的不同。让我通过具体案例来说明:

案例演示

1
2
3
4
5
6
7
8
9
10
int A = 5;    // 二进制: 0000 0101
int B = 3; // 二进制: 0000 0011

// 情况1: A & B
int result1 = A & B; // 5 & 3
Console.WriteLine($"A & B = {result1}"); // 输出: 1

// 情况2: -A & B
int result2 = -A & B; // -5 & 3
Console.WriteLine($"-A & B = {result2}"); // 输出: 3

二进制分析:

1
2
3
4
5
6
7
A = 5:  0000 0101
B = 3: 0000 0011
A & B: 0000 0001 = 1

-A = -5: 1111 1011 (补码表示)
B = 3: 0000 0011
-A & B: 0000 0011 = 3

更多案例

1
2
3
4
5
6
7
8
9
10
11
12
// 案例2
int A2 = 10; // 1010
int B2 = 6; // 0110

Console.WriteLine($"{A2} & {B2} = {A2 & B2}"); // 输出: 2
Console.WriteLine($"-{A2} & {B2} = {-A2 & B2}"); // 输出: 6

// 案例3 - 负数和负数
int A3 = -3;
int B3 = -5;

Console.WriteLine($"{A3} & {B3} = {A3 & B3}"); // 输出: -7

背后的意义

1. 补码表示法

  • 在C#中,整数使用二进制补码表示
  • 正数的补码是其本身
  • 负数的补码 = 对应正数的二进制取反 + 1

2. 运算本质

  • A & B:对两个数的实际二进制位进行按位与
  • -A & B:先计算A的补码(得到-A),再与B进行按位与

3. 实际应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 标志位检查
[Flags]
enum Permissions
{
Read = 1, // 0001
Write = 2, // 0010
Execute = 4 // 0100
}

// 检查权限
Permissions userPermissions = Permissions.Read | Permissions.Write;

// 正确的方式:直接与操作
bool canRead = (userPermissions & Permissions.Read) != 0; // true

// 错误的方式:使用负数(无意义)
bool wrongCheck = (-(int)userPermissions & (int)Permissions.Read) != 0; // 结果不可预测

4. 重要注意事项

1
2
3
4
// 注意:-A & B 不等于 -(A & B)
int A = 5, B = 3;
Console.WriteLine($"-A & B = {-A & B}"); // 3
Console.WriteLine($"-(A & B) = {-(A & B)}"); // -1

总结

  • A & B 是直接的按位与运算,结果可预测
  • -A & B 涉及补码转换,结果取决于负数的二进制表示
  • 在实际编程中,应避免对负数进行无意义的位运算,除非明确理解补码机制
  • 位运算通常用于标志位、掩码操作等场景,应保持操作数的明确性和可读性