C# is a high-level language and therefore one rarely needs to work with single bits. Nevertheless, C# offers all the possibilities to do so. Bit manipulations can be used for many purposes, like with Enums, low level hardware communication or mathematics.
Binary Literals
Binary literals were introduced with C# 7.0. This allows constant values for Integral numeric types to be specified in a binary form. Together with the new digit separator, also added with C# 7.0, values can be formatted in a nice way:
byte byteValue = 0b0000_0001;
short shortValue = 0b0000_0000_0000_0010;
int intValue = 0b0000_0000_0000_0000_0000_0000_0000_0011;
long longValue = 0b0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0100;
When the most significant bit (MSb) is set, the constant is interpreted as an unsigned type:
// Int32 because the MSb is not set
int value1 = 0b0111_1111_1111_1111_1111_1111_1111_1111;
// also Int32 because the MSb is not set
int value2 = 0b1111;
// UInt32 because the MSb is set
uint value3 = 0b1111_1111_1111_1111_1111_1111_1111_1111;
Operators
For the work with single bits, there are bitwise and shift operators. These can be applied on integral numeric types and char. C# does support the following operators:
-
Bitwise complement operator
~
: Reverses all bits, 1 becomes 0 and 0 becomes 1. -
Left-shift operator
<<
: Shifts all bits to the left by a specified number of places. This causes bits to fall out on the left and 0 to be inserted on the right. -
Right-shift operator
>>
: Shifts all bits to the right by a specified number of places. This causes bits to fall out on the right. What is inserted on the left depends on the type and value. For signed types 1 is inserted if the number is negative (MSb is 1) or 0 for positive numbers or 0. For unsigned types 0 is always inserted. -
Unsigned right-shift operator
>>>
: As described, the regular right-shift operator determines what is inserted on the left depending on the type or value of the MSb. With C# 11.0 this new operator was added, which always inserts 0 on the left side. -
Logical AND operator
&
: With this operator the individual bits per position are compared. If both bits are 1, the result bit is also 1 and otherwise 0. This is done for all bits individually. -
Logical OR operator
|
: With this operator the individual bits per position are compared. If at least one of the bits is 1, the result is 1 and otherwise 0. This is done for all bits individually. -
Logical XOR operator
^
: With this operator the individual bits per position are compared. Only if exactly one of the bits is 1, the result is 1 and otherwise 0. This is done for all bits individually.
Here is some code:
//
// Bitwise complement operator ~
//
int value = ~0b0000_1111_0000_1111_0000_1111_0000_1111;
// value -> 1111 0000 1111 0000 1111 0000 1111 0000
//
// Left-shift operator <<
//
int value = unchecked((int)0b1000_0000_0000_0000_0000_0000_0000_0001) << 4;
// value -> 0000 0000 0000 0000 0000 0000 0001 0000
//
// Right-shift operator >> with 1 as MSb
//
int value = unchecked((int)0b1000_0000_0000_0000_0000_0000_0000_0001) >> 4;
// value -> 1111 1000 0000 0000 0000 0000 0000 0000
//
// Right-shift operator >> with 0 as MSb
//
int value = 0b0000_1000_0000_0000_0000_0000_0000_0001 >> 4;
// value -> 0000 0000 1000 0000 0000 0000 0000 0000
//
// Right-shift operator >> with 1 as MSb and unsigned type
//
uint value = 0b1000_0000_0000_0000_0000_0000_0000_0001 >> 4;
// value -> 0000 1000 0000 0000 0000 0000 0000 0000
//
// Right-shift operator >> with 0 as MSb and unsigned type
//
uint value = 0b0000_1000_0000_0000_0000_0000_0000_0001 >> 4;
// value -> 0000 0000 1000 0000 0000 0000 0000 0000
//
// Unsigned right-shift operator >>> with 1 as MSb
//
int value = unchecked((int)0b1000_0000_0000_0000_0000_0000_0000_0001) >>> 4;
// value -> 0000 1000 0000 0000 0000 0000 0000 0000
//
// Unsigned right-shift operator >>> with 0 as MSb
//
int value = 0b0000_1000_0000_0000_0000_0000_0000_0001 >>> 4;
// value -> 0000 0000 1000 0000 0000 0000 0000 0000
//
// Logical AND operator &
//
int value = 0b0000_1111_0000_1111_0000_1111_0000_1111 & 0b0101_0101_0101_0101_0101_0101_0101_0101;
// value -> 0000 0101 0000 0101 0000 0101 0000 0101
//
// Logical OR operator |
//
int value = 0b0000_1111_0000_1111_0000_1111_0000_1111 | 0b0101_0101_0101_0101_0101_0101_0101_0101;
// value -> 0101 1111 0101 1111 0101 1111 0101 1111
//
// Logical XOR operator ^
//
int value = 0b0000_1111_0000_1111_0000_1111_0000_1111 ^ 0b0101_0101_0101_0101_0101_0101_0101_0101;
// value -> 0101 1010 0101 1010 0101 1010 0101 1010
Conclusion
Working with single bits does not happen too much, but C# has all the possibilities to get things done. The binary literal feature is useful for some constants, like Enum values.
You can find some code examples in my GitHub repository.