On the lowest scale all information is stored in the form of 1s and 0s. Although you will be working with strings and collections and objects when programming in C# you may need to access these bits at some point. This could be for networking purposes or maybe working with hardware or encryption. Bitwise operations can also come in handy when dealing with bitmaps and manipulating flags. C# has provided us with several bitwise operators and shift operators to work with integers (byte, int, uint, long, etc.)

When you use the bitwise complement operator ~ on an integer it will change all 0 bits to 1 and 1 bits to 0. Basically it does the exact same thing as the not operator. In C# if your using signed integers they use two’s complement which means the highest order bit is used for the sign. So if we take int a = 5 or in binary ...00000101. When we flip all the bits we end up with ...11111010. Since we flipped all the bits we changed our sign so a = -6.

int original = 5;       // ...0000 0101
int inverted = ~original; // ...1111 1010 (-6 in decimal)

The bitwise AND operator (&) compares two operands bit by bit and yields a 1 at a given bit position only if both corresponding bits in the operands are 1. If either bit (or both) is 0, the resulting bit is 0. A primary real-world application of & is bit masking—extracting or testing specific bits while ignoring the rest. For instance, evaluating 12 & 10 compares 1100 and 1010 in binary; only the bit in the $2^3$ position (the eights place) has a 1 in both numbers, yielding 1000 (8 in decimal):

int a = 12; // 0000 1100
int b = 10; // 0000 1010
int resultAnd = a & b; // 0000 1000 (8 in decimal)

// Checking if a specific flag is set
bool hasThirdBit = (a & (1 << 2)) != 0; // Evaluates to true

The bitwise OR operator (|) compares two operands and produces a 1 if at least one of the corresponding bits is 1. It only outputs 0 when both corresponding bits are 0. This operator is widely used to combine multiple flags or force specific bits to be set to 1 without disturbing surrounding bits. If you combine 9 (1001) and 5 (0101) using |, the result contains a 1 wherever either operand has a 1, producing 1101 (13 in decimal):

int permissions = 0;
int read = 1;    // 0001
int write = 2;   // 0010

// Combine permissions
permissions = read | write; // 0011 (3 in decimal)

The bitwise exclusive OR operator (^), commonly called XOR, compares two operands and yields 1 if and only if the corresponding bits are different. If both bits are 0 or both bits are 1, it evaluates to 0. XOR has unique mathematical properties: any value XORed with 0 remains unchanged, and any value XORed with itself becomes 0. This makes it ideal for toggling bits, parity checks, simple symmetric obfuscation, and swapping values without temporary variables. For example, computing 14 ^ 9 compares 1110 with 1001, producing 0111 (7 in decimal):

int x = 14; // 1110
int y = 9;  // 1001
int resultXor = x ^ y; // 0111 (7 in decimal)

// Toggling a bit on and off
int state = 5;       // 0101
state = state ^ 1;   // 0100 (toggled lowest bit to 0)
state = state ^ 1;   // 0101 (toggled lowest bit back to 1)

Complementing bitwise logic are the shift operators, which slide binary patterns horizontally across memory positions. The left-shift operator (<<) shifts all bits of its left operand to the left by the number of places specified by its right operand, filling vacated positions on the right with zeros. High-order bits that shift beyond the boundary of the type are discarded. Shifting left by $n$ positions is mathematically equivalent to multiplying by $2^n$ (provided no overflow occurs into the sign bit). For instance, shifting 3 (0011) left by 2 positions produces 1100 ($3 \times 2^2 = 12$):

int value = 3;             // 0000 0011
int shiftedLeft = value << 2; // 0000 1100 (12 in decimal)

Conversely, the right-shift operator (>>) moves bits to the right by the specified count, discarding bits pushed off the right edge. In C#, the behavior of the vacated high-order bits depends on whether the operand is signed or unsigned. For unsigned types (uint, ulong), >> performs a logical shift, always filling new high-order bits with 0. For signed types (int, long), >> performs an arithmetic shift, preserving the sign bit by replicating the leftmost bit (a 1 for negative numbers, a 0 for positive numbers). Right-shifting by $n$ acts as integer division by $2^n$. Shifting 20 (00010100) right by 2 positions results in 00000101 ($20 / 2^2 = 5$):

int positiveVal = 20;            // 0001 0100
int shiftedRight = positiveVal >> 2; // 0000 0101 (5 in decimal)

int negativeVal = -16;
int shiftedNegative = negativeVal >> 2; // Preserves sign bit, yields -4

Starting with C# 11 and .NET 7, C# introduced the unsigned right-shift operator (>>>), which enforces a logical right shift regardless of whether the operand is signed or unsigned. This operator always shifts in zeros from the left, ignoring sign preservation. When applied to negative numbers, this treats the sign bit as pure data, converting negative integers into large positive values, which eliminates the need to cast signed values to unsigned types just to achieve a zero-filling right shift:

int signedNegative = -16; // Binary representation starts with 1
int logicalShift = signedNegative >>> 2; 
// The two leftmost bits become 00 instead of 11, resulting in 1073741820

There are some things to note about the C# runtime when working with bit wise operators and writing code that will be considered proper usage of these operators. The first being that if your dealing with any integral value less than an int. The values will be cast to ints and evaluated. If you need to store the value in the original type make sure to cast your answer. byte b = (byte)(b1 & b2); All binary operators also have there compound counterparts that can be used as well. These are very useful when trying to mask bits or set flags for your system.

Shares:

Leave a Reply