Bitwise Operations in C#
Author: Oğuzhan KARAGÜZEL · August 13, 2023

INTRODUCTION
CLICK HERE TO ACCESS THE CODE IN THIS ARTICLE
Hello.
After a long, exhausting software development training process, I have finally reached the point where I can build products and I have become a star developer. But to truly call myself a developer, I made it my duty to dig down to the foundations of what we do. In this article, in order to reinforce what I know and to guide those who think like me, I want to explain bit-based operations and do a simple exercise. I have to warn you up front! I will try to cover every detail, so this is going to be a fairly long article.
People who, like me, have just completed their software training or are still in it either do not know this topic or have only heard its name. For that reason, I want to start with the question “what is a bit and how do we define it?”
BIT (binary digit): binary place, binary number system, or binary numeral. In other words, base two. Actually, however you define it, it will be correct. According to Prof. Dr. Emrah Sefa Gürkan, “digit” is in fact of French origin and means “finger.”
Since this term entered computer science and is used to mean “place/position,” its meaning has now changed. For that reason, I think “base two (binary)” is the most accurate rendering, and I will use it that way throughout the article.
Even the counting numbers and digits we use in daily life are built on the “base ten” system. Let us list them: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. (Notice there are ten of them!) The answer to “why base ten?” is that we have ten fingers. There is no need to cite a reference for this answer; it is now common knowledge. Now, if we count using these counting numbers, when we reach nine we have no counting numbers or digits left, so we carry over by writing a one to its left. This is a very consistent and logical system. We have ten counting numbers. Just like carving notches on a wall, when the numbers run out we move to the left. Let us try to do the same thing in base two. This time we have only two counting numbers. Let us list them: 0, 1.
We still have the other digits available! (2, 3, 4, 5, 6, 7, 8, 9). But we only have two numbers. In that case, if we count in binary and decimal, we get the following.
binary 0 = decimal 0
binary 1 = decimal 1
Now the counting numbers in base two are exhausted. If we continue by applying the operations we are used to from base ten;
binary 10 = decimal 2
binary 11 = decimal 3
binary 100 = decimal 4
binary 101 = decimal 5
binary 110 = decimal 6
binary 111 = decimal 7
binary 1000 = decimal 8
binary 1001 = decimal 9
Now the counting numbers, and even the digits, in base ten are exhausted too.
binary 1010 = decimal 10
BINARY TO DECIMAL CONVERSION
This operation is quite easy. After a short clarification of base ten, let us look at base two.
If we examine the number 354;
354 = (3*100) + (5*10) + (4*1) = (3*(10²)) + (5*10¹) + (4*10⁰)
as you can see, since our base is “10,” we can complete this operation by raising the value each place holds to the appropriate power. If we show that binary 1001 = decimal 9 through conversion;
(1*(2³)) + (0*(2²)) + (0*(2¹)) + (1*(2⁰)) = 8 + 0 + 0 + 1 = 9
can be seen.
DECIMAL TO BINARY CONVERSION
This operation is also quite simple. You can convert any number you like into any base you like. You just need to repeatedly divide the number by the base you want to convert to. Our work becomes easier if we examine the parts of the division.
The number we want to convert (the dividend).
The base we want to convert to (the divisor).
The number we want to convert for the (n+1)th operation (the quotient, the (n+1)th dividend).
Our number whose place value is “n” in the base we want to convert to (the remainder).
It will be easier to understand if we put it into a mathematical operation. As an example, let us again convert the number and digit 9 (the reason I emphasize “digit” is that in the hexadecimal system there are no digits left, so we use letters and call them digits!) to base two.
if we start in order.
(n = 0) for 9/2, quotient = 4 and remainder = 1
(n = 1) for 4/2, quotient = 2 and remainder = 0
(n = 2) for 2/2, quotient = 1 and remainder = 0
(n = 3) for 1/2, quotient = 0 and remainder = 1
once the quotient reaches 0, there is no point in continuing the operation. Anyway, to the left of every number there is a “0” that goes on forever. If we write the remainders we obtained in order according to their place value, we get the number “1001.” This shows that our operation is correct.
Assuming this part is understood, if I explain how a bit-based value is assigned to a variable in C#, we will have completed the groundwork for our main topic.
BIT-BASED VARIABLE ASSIGNMENTS IN C#
The most correct thing will be to show this operation directly by coding it;
// Bit temelli değişken atama
// Bit-based variable assignment
int bitWise = 0b1001;
Console.WriteLine("int bitWise = 0b1001; => bitwise = " + bitWise);
Console.WriteLine("****************************************************************************");
// C# bit temelli değişken atamalarında, atama işlemini kolaylaştırmak adına, "_" kullanımını münkün kılar ve sayıları 4'lü gruplar halinde yazabiliriz.
// In C# bit-based variable assignments, we can use "_" to facilitate assignment and write numbers in groups of 4.
bitWise = 0b_1001;
Console.WriteLine("int bitWise = 0b_1001; => bitwise = " + bitWise);
Console.WriteLine("****************************************************************************");
The output of these lines is as follows. int bitWise = 0b1001; => bitwise = 9
****************************************************************************
int bitWise = 0b_1001; => bitwise = 9
**************************************************************************** As you can see, the only rule for writing the number 9 in binary is this: before defining the number, we must state that it is bit-based by writing “0b”. If we want to assign it in hexadecimal instead, writing “0x” is enough.
// Hexadecimal atama için
// For hexadecimal assignment
int hexaDecimal = 0x_1001;
Console.WriteLine("int hexaDecimal = 0x_1001; => hexaDecimal = " + hexaDecimal);
The output of these lines is as follows. int hexaDecimal = 0x_1001; => hexaDecimal = 4097 So what if we want to print a normally-assigned decimal number in other bases? The answer is as follows;
// Rastgele onluk bir sayıyı diğer tabanlarda yazdırma.
// Printing a random decimal number in other bases.
// "decimal" bir anahtar kelimedir kullanılamaz!!!
// "decimal" is a keyword cannot be used!!!
Random rnd = new Random();
int random = rnd.Next(1, 255);
string binaryBase = Convert.ToString(random, 2);
string octalBase = Convert.ToString(random, 8);
string baseTen = Convert.ToString(random, 10);
string hexaDecimalBase = Convert.ToString(random, 16);
Console.WriteLine(
"number = " + random +
"\nbinaryBase = " + binaryBase +
"\noctalBase = " + octalBase +
"\nbaseTen = " + baseTen +
"\nhexaDecimalBase = " + hexaDecimalBase);
the output of these lines is as follows.
number = 219
binaryBase = 11011011
octalBase = 333
baseTen = 219
hexaDecimalBase = db
And it is exactly at this point that we reach a wonderful place.
HEXADECIMAL BASE
In the example above, we saw that the number “219” is “db” in hexadecimal. So why db?
First, let us briefly explain why the hexadecimal base exists and why we use it;
when defining numbers in binary, for convenience we had separated the numbers into groups of four using “_”. In fact, the whole thing starts from these groups of four. The memory unit of the computer you are using right now is, very very likely, the byte. And 1 byte consists of 8 bits. So, in computer science we call every 8-digit binary number a byte, and in programming languages we also use it as a variable named “byte.” The smallest eight-digit binary number is “00000000,” that is, 0 in decimal. The largest eight-digit binary number is “11111111,” which is 255 in decimal.
In the early stages of computer technology, groupings like this were used to record data. To not stray from the subject, let me mention it briefly; groups of 4, groups of 5, groups of 6, and groups of 8 (byte) were used as units of measure. Today, the group of 8 is predominantly used. According to a weak theory, the word “Byte” is actually short for “by eight.” If you look at the “ASCII” table, basically every number from 0 up to 127 has a counterpart. This is in fact a product of byte-based data grouping. Why 128 of them? That is a whole other topic! Maybe in another article. The answer to “why is this grouping done?” is as follows; so that the processor, when reading data from memory, can make sense of that data. More precisely, it is done this way so that we humans can build this system properly. Otherwise, the thing you call a processor is nothing but an electrical switch. As an example, look at your computer’s specifications. Very likely it will say the processor is x64 or 64-bit based. This means it is the amount of data your processor reads from memory in a single clock cycle. 64/8 = 8 bytes. That is, the processor reads 8 bytes of data from memory at once and evaluates each byte on its own. That is enough detail. Let us now return to our topic.
This group of four here is a very important group. I will explain why and then not stray from the topic. If we examine this group of four, it consists of 4 bits, and its smallest value in decimal is “0000”2 = “0”10. Its largest value is “1111”2 = “15”10. Because this system of four is used a lot and is important, the numbers from 0 to 15 began to be used as a base-16 system. Let us list them (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15). As you can see, all the numbers and digits of the hexadecimal system. But there is a serious problem here! We can write numbers in hexadecimal this way, but we cannot read them. To read them, we need digits. For example, if I tell you that the number 256 is in hexadecimal, you can easily convert it to decimal and there will be no problem. But the number 111 becomes a serious problem. In this way, the decimal equivalent of the number 111;
⇒ (1*(16²)) + (1*(16¹)) +(1*(16⁰)) = 273
⇒ (11*(16¹)) + (1*(16⁰)) = 177
⇒ (1*(16¹)) + (11*(16⁰)) = 27
could be any one of these values. Since we cannot know which, new digits had to be invented. But they were not invented, and the new numbers and digits of the hexadecimal system were defined as follows;
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f).
This way, we became able to represent every group of four with a single digit. This provides an important benefit in programming. For example, when we define a variable, a certain area is reserved in RAM and the address information is represented by the variable name. This address information, instead of gigantic binary numbers, is expressed with shorter hexadecimal numbers. This benefit is a small part of what falls within our area of interest. In computer science, its place is much larger. Let us see this too with a short example;
// Bellek Adresleri
// Memory Addresses
int value = 42;
unsafe
{
int* ptr = &value;
Console.WriteLine($"Değer - value: {value}");
Console.WriteLine($"Bellek Adresi - Memory Address: 0x{(ulong)ptr:x}");
}
You cannot run this code block directly. To run it, you need to go into your project’s settings and have the “allow unsafe code” option checked. But you don’t need to do that. I will give you the result anyway. The output of these lines is as follows.
Değer — value: 42
Bellek Adresi — Memory Address: 0x945558e58c
In the early stages of computer science, when programming was done using the “Assembly” language, these address values were entered by hand. What do you think is the binary equivalent of the number 945558e58c? Answer; 100101000101010101001110100010001100
For this reason, the hexadecimal system is important in computer science. I hope it has been explanatory enough. Thanks to today’s programming languages, since we write only a variable name instead of an address, we are not even aware of this problem. But keep it in mind. It is actually an address.
Let us continue our topic with bitwise operations.
BITWISE OPERATORS
First, let us give the operator information;
1 - The Unary ~ (bitwise complement) operator:
This operator produces the complementary (inverted) value of every bit of a number. That is, a 0 bit becomes 1, and a 1 bit becomes 0. For example, the expression ~5, if thought of as ~00000101, will result in 11111010.
2 - The Binary << (left shift), >> (right shift) and >>> (unsigned right shift) operators:
⇒ Left Shift (<<): Shifts the bits of a number or bit sequence to the left by a certain number of steps. Each shift operation has the effect of multiplying the number by 2.
⇒ Right Shift (>>): Shifts the bits of a number or bit sequence to the right by a certain number of steps. Each shift operation has the effect of dividing the number by 2.
⇒ Unsigned Right Shift (>>>): This operator, found not only in C# but also in languages like Java, is the unsigned right-shift operation. It shifts over unsigned numbers instead of signed ones. In C# 10.0 this operator does not exist directly; the right-shift operator >> is used. However, according to what Microsoft states on its website, this feature arrived with C# 11. Since I ran this code with .NET 6.0, I will write the operations related to this operator in line with the information Microsoft provides on its website. I will include these links in the References.
3 - The Binary & (logical AND), | (logical OR) and ^ (logical exclusive OR) operators:
⇒ Logical AND (&): Compares each bit of two numbers or bit sequences and sets the result bit to 1 if both bits are 1.
⇒ Logical OR (|): Compares each bit of two numbers or bit sequences and sets the result bit to 1 if at least one bit is 1.
⇒ Logical Exclusive OR (^): Compares each bit of two numbers or bit sequences and sets the result bit to 1 if the bits differ from each other.
These operators are used in bit manipulation, data processing, and other low-level operations. Understanding the behavior of these operators is important when performing bit-based operations.
BITWISE OPERATIONS
Now let us try to perform operations using these operators and to understand these operations.
THE ~ OPERATOR
First, let us use the logical inverter and try to invert the number 5;
// ~
int a = 5;
int b = ~a;
Console.WriteLine(
"sayı - value = " + a +
"\nikilik - binary = " + Convert.ToString(a, toBase: 2) +
"\nTerslenmiş hali - inverted state = " + b +
"\nbit bazında terslenmiş hali - bitwise inverted = " + Convert.ToString(b, toBase: 2));
The output of these lines is as follows;
sayı — value = 5
ikilik — binary = 101
Terslenmiş hali — inverted state = -6
bit bazında terslenmiş hali — bitwise inverted = 11111111111111111111111111111010
Now we have arrived at a confusing point. Let us explain it one by one;
First, why did the number 101, after being inverted, become 11111111111111111111111111111010? The “int” variable type represents a 32-bit value in C#. When we print the value, even if that value is “101”, it is actually “00000000000000000000000000000101”. Because every bit is inverted, the result 11111111111111111111111111111010 is obtained. This is quite consistent, because if you look closely, every 0 became a 1 and every 1 became a 0.
The second question is how the inverted form of the value 5 turns out to be -6. While the decimal value of 00000000000000000000000000000101 is 5, the decimal value of 11111111111111111111111111111010 should be 4,294,967,290 — so how did it become -6?
The answer is as follows; the leftmost bit, or the 32nd bit, is the sign bit. If the 32nd bit is 0 the number is positive, and if it is 1 the number is negative. That is precisely why the resulting number is negative. This is a very important piece of information that you must keep in mind. Also, since the 32nd bit is the sign bit, should -6 not then be 10000000000000000000000000000110? No, it should not! Processors do not compute mathematical operations such as addition and subtraction by adding and subtracting the way we know it. They do it entirely with logical operations. For that reason, for a computer the largest negative value is 1111111111111111111111111111111 = -1, while the smallest negative value is 10000000000000000000000000000000 = -2,147,483,648. Let us write the necessary lines and examine them.
// min - max, negative integers
int min = int.MinValue;
int max = -1;
Console.WriteLine("min - max, negative integers; " +
"\nmin = " + min +
"\nBinary min = " + Convert.ToString(min, 2) +
"\nmax = " + max +
"\nBinary max = " + Convert.ToString(max, 2)
);
The output of these lines is as follows;
min — max, negative integers;
min = -2147483648
Binary min = 10000000000000000000000000000000
max = -1
Binary max = 11111111111111111111111111111111
As I said before, the reason is that we perform addition and subtraction entirely with logical operations, so the numbers are defined this way. I will not go into how addition and subtraction are done with logical operations here. We would stray too far from our topic, and there are quite good resources available on the subject.
To avoid confusion, from here on I will continue with the “uint” variable. In this variable type, the 32nd bit is a normal digit and has nothing to do with the sign.
If we redefine and run the same lines with the “uint” variable type, the result is;
// ~
uint a = 5;
uint b = ~a;
Console.WriteLine(
"sayı - value = " + a +
"\nikilik - binary = " + Convert.ToString(a, toBase: 2) +
"\nTerslenmiş hali - inverted state = " + b +
"\nbit bazında terslenmiş hali - bitwise inverted = " + Convert.ToString(b, toBase: 2));
Console.WriteLine("****************************************************************************");
sayı — value = 5
ikilik — binary = 101
Terslenmiş hali — inverted state = 4294967290
bit bazında terslenmiş hali — bitwise inverted = 11111111111111111111111111111010
As you can see, although we obtain the same values at the bit level, this time the result is not -6 but 4294967290, exactly as we calculated above.
<< LEFT SHIFT
This is quite a simple operation. If we show it right away by coding it;
// Sola kaydırma - Lest shift
uint number1 = 5;
uint leftShifted = number1 << 1;
Console.WriteLine(
"number = " + number1 +
"\nleftShifted = " + leftShifted +
"\nBitwise number1 = " + Convert.ToString(number1, 2) +
"\nBitwise leftShifted = " + Convert.ToString(leftShifted, 2)
);
If we explain here briefly; the variable number is the variable to be shifted. The 1 is the shift amount. Briefly, like this: number << ?. number = the number to be shifted. ? = the shift amount. In light of this information, if we look at the outputs of the lines;
number1 = 5
leftShifted = 10
Bitwise number = 101
Bitwise leftShifted = 1010
Exactly as we expected. As you can see, in the shift operation we essentially multiplied the number by 2. We can say it is as if 5 * 2 = 10. This is quite consistent. We know that the binary number 101 is 5. If we shift each of its places one to the left, we get 1010. This number is also equal to 10. So why 1010 and not 1011? I don’t think I need to answer this question! If it were 1011, then it would make sense to ask this question!
>> RIGHT SHIFT
This time let us shift the number 10 to the right. Let us see whether we can obtain the number 5?
// Sağa kaydırma - Right shift
uint number2 = 10;
uint rightShifted = number2 >> 1;
Console.WriteLine(
"number2 = " + number2 +
"\nrightShifted = " + rightShifted +
"\nBitwise number2 = " + Convert.ToString(number2, 2) +
"\nBitwise rightShifted = " + Convert.ToString(rightShifted, 2)
);
If we look at the output of these lines;
number2 = 10
rightShifted = 5
Bitwise number2 = 1010
Bitwise rightShifted = 101
Exactly as we expected. So what happens if this time we shift by 2 places instead of 1? When we divide 5 by 2 we get 2.5. So which result will the computer give us? Since we are working with integers, it must give either 2 or 3. If we shift the binary number 101 one more step, we obtain the binary number 10. That gives us the result 2. Is that so? Let us edit the code above and try;
// iki kere Sağa kaydırma - shifted right twice
uint number3 = 10;
uint shiftedRightTwice = number3 >> 2;
Console.WriteLine(
"number3 = " + number3 +
"\nshiftedRightTwice = " + shiftedRightTwice +
"\nBitwise number3 = " + Convert.ToString(number3, 2) +
"\nBitwise shiftedRightTwice = " + Convert.ToString(shiftedRightTwice, 2)
);
The output of these lines is as follows;
number3 = 10
shiftedRightTwice = 2
Bitwise number3 = 1010
Bitwise shiftedRightTwice = 10
Exactly as we expected. If you asked earlier, the answer to the question is very clear. The computer always rounds down. You saw why with your own eyes. Since shifting right means dividing by 2, let us also try this with negative integers and stir up a little confusion. For example;
// İşaretki sağa kaydırma - Signed right shift
int number4 = -10;
int signedRightShifted = number4 >> 1;
Console.WriteLine(
"number4 = " + number4 +
"\nsignedRightShifted = " + signedRightShifted +
"\nBitwise number4 = " + Convert.ToString(number4, 2) +
"\nBitwise signedRightShifted = " + Convert.ToString(signedRightShifted, 2)
);
The output of these lines is as follows;
number4 = -10
signedRightShifted = -5
Bitwise number4 = 11111111111111111111111111110110
Bitwise signedRightShifted = 11111111111111111111111111111011
As you can see, even when signed, the division by 2 still took place. Again this is quite consistent. I am aware of the question forming in your mind. But let us check whether the premise “it always rounds down” is correct. Here, do not confuse the notion of “down.” If we shift twice and it rounds down, we should obtain -3. Let us see whether that is so. For example;
// işaretli iki kere Sağa kaydırma - Signed shifted right twice
int number5 = -10;
int signedRightShiftedTwice = number5 >> 2;
Console.WriteLine(
"number5 = " + number5 +
"\nsignedRightShiftedTwice = " + signedRightShiftedTwice +
"\nBitwise number5 = " + Convert.ToString(number5, 2) +
"\nBitwise signedRightShiftedTwice = " + Convert.ToString(signedRightShiftedTwice, 2)
);
Console.WriteLine("****************************************************************************");
The output of these lines is as follows;
number5 = -10
signedRightShiftedTwice = -3
Bitwise number5 = 11111111111111111111111111110110
Bitwise signedRightShiftedTwice = 11111111111111111111111111111101
Exactly as we expected. Now let us come to our question. Why, when right-shifting negative integers, is the 32nd bit continuously set to 1 instead of 0? With unsigned or positive integers, the right-shift operation continuously added a 0 from the right and deleted the 0th bit from the left. But when signed, the right-shift assigns according to the sign. To understand this situation better, let us left-shift a negative integer and examine the result. Shifting left means multiplying by 2. If we multiply -10 by 2 we should get -20. Let us see whether our premise still holds. The example is as follows;
// işaretli sola kaydırma - Signed shifted left
int number6 = -10;
int signedLeftShifted = number5 << 1;
Console.WriteLine(
"number6 = " + number6 +
"\nsignedLeftShifted = " + signedLeftShifted +
"\nBitwise number6 = " + Convert.ToString(number6, 2) +
"\nBitwise signedLeftShifted = " + Convert.ToString(signedLeftShifted, 2)
);
Console.WriteLine("****************************************************************************");
The output of these lines is as follows;
number6 = -10
signedLeftShifted = -20
Bitwise number6 = 11111111111111111111111111110110
Bitwise signedLeftShifted = 11111111111111111111111111101100
Our premise still holds. This time a 0 is added from the right and a digit is deleted from the left.
Important note: The rightmost bit, that is the bit whose place value is 0, is called the least significant bit. Likewise the leftmost bit — in our example the 32nd bit, or the bit whose place value is 31 — is called the most significant bit.
There is no point in explaining more about this topic. After these explanations, the unsigned right-shift operator >>> will be understood quite easily.
>>> THE UNSIGNED RIGHT-SHIFT OPERATOR.
I cannot say I know for certain, but if you run this code in a console app on .NET 7.0, perhaps it will run without errors. Or you may manage to run the code through the Microsoft link I gave in the references.
Above, we performed bit-level right and left shifts of signed and unsigned integers, or of positive and negative integers, and proved them by offering various premises. Our premises are as follows;
A positive or negative integer, even when shifted left or right at the bit level, invariably;
⇒ Shifting right is equivalent to dividing the number by 2.
⇒ Shifting left is equivalent to multiplying the number by 2.
⇒ Regardless of sign, after dividing odd numbers by 2, the resulting quotient is always rounded down.
So do these premises hold for the unsigned right shift? Or, since there is an unsigned right shift, why is there no unsigned left shift?
We saw in the examples above that in the right-shift operation the shift is done according to the value of the sign bit. In more basic terms; shift operations are always performed so as to obey the premises above.
However, the unsigned right shift does not obey these premises. Let us examine this example;
int x = -8;
Console.WriteLine($"Before: {x,11}, hex: {x,8:x}, binary: {Convert.ToString(x, toBase: 2), 32}");
int y = x >> 2;
Console.WriteLine($"After >>: {y,11}, hex: {y,8:x}, binary: {Convert.ToString(y, toBase: 2), 32}");
int z = x >>> 2;
Console.WriteLine($"After >>>: {z,11}, hex: {z,8:x}, binary: {Convert.ToString(z, toBase: 2).PadLeft(32, '0'), 32}");
// Output:
// Before: -8, hex: fffffff8, binary: 11111111111111111111111111111000
// After >>: -2, hex: fffffffe, binary: 11111111111111111111111111111110
// After >>>: 1073741822, hex: 3ffffffe, binary: 00111111111111111111111111111110
As you can see, the number -8 is shifted two places to the right in the normal way, and, in a manner consistent with both our expectations and our premises, the result comes out as -2. However, the variable z does not conform to our premises and expectations. From this we understand that; the unsigned right-shift operation, regardless of the value of the sign, continuously writes a 0 from the left as if shifting a positive integer to the right. That is, even the sign bit — the 32nd bit, or the bit whose place value is 31 — changes and becomes zero. For this reason a negative integer becomes positive.
Thanks to the abundant examples and explanations we made above, I think this situation is easily understood. For that reason there is no point in dwelling on this topic. After giving the following small note, it is best to end this topic. Our example is as follows;
// Bit temelli işaretli ve işaretsiz atama
// Bitwise signed and unsigned assignment
// Bu şekilde atama yapılamaz - It cannot be assigned in this way.
int number7 = 0b_1000_1111_0000_1111_0000_1111_0000_1100;
// hata - error = Cannot implicitly convert type 'uint' to 'int'. An explicit conversion exists (are you missing a cast?)
// Bu şekilde atama yapılabilir - This way you can assign
int number8 = 0b_0000_1111_0000_1111_0000_1111_0000_1100;
// veya - or
uint number9 = 0b_1000_1111_0000_1111_0000_1111_0000_1100;
uint number10 = 0b_0000_1111_0000_1111_0000_1111_0000_1100;
THE & LOGICAL “AND” OPERATOR
This operator obtains a result by putting numbers through the “and” logical operation bit by bit. In fact, since in computer science all variables are numbers, you can apply this operator not only to numbers but to all types of variables you know. Let us quickly do an example. For example, to make it clearer and shorter, if we keep the numbers small;
// & operatörü - & operators
int number11 = 10;
int number12 = 7;
int number13 = number11 & number12;
Console.WriteLine(
"number11 = " + number11 +
"\nnumber12 = " + number12 +
"\nnumber13 = number11 & number12 = " + number13 +
"\nBitwise number11 = " + Convert.ToString(number11, 2) +
"\nBitwise number12 = " + Convert.ToString(number12, 2) +
"\nBitwise number13 = " + Convert.ToString(number13, 2)
);
Console.WriteLine("****************************************************************************");
The output of these lines is as follows;
number11 = 10
number12 = 7
number13 = number11 & number12 = 2
Bitwise number11 = 1010
Bitwise number12 = 111
Bitwise number13 = 10
If the outputs are examined, what happened can be seen very clearly. We have a 32-bit variable and each bit was compared with its counterpart to obtain the result. Now let us examine this in more detail. Let “n” be our place value. Also, the binary of 10 is 1010 and the binary of 7 is 111. If we compare these numbers at the bit level;
♦ (n = 0) “0” for 10 & “1” for 7 => 0&1 = 0 is obtained.
♦ (n = 1) “1” for 10 & “1” for 7 => 1&1 = 1 is obtained.
♦ (n = 2) “0” for 10 & “1” for 7 => 0&1 = 0 is obtained.
♦ (n = 3) “1” for 10 & “0” for 7 => 1&0 = 0 is obtained.
♦ (n = 4) “0” for 10 & “0” for 7 => 0&0 = 0 is obtained.
There is no need to continue further here. We know it is 0 up to the 32nd bit. The number obtained here is as follows;
00000000000000000000000000000010 = 2, or binary 10 = decimal 2.
Important note: the “and” operation is never ever the subtraction of numbers! It cannot be likened to it, and indeed should never be likened to it!
I think this operation is understood. Since there is nothing more to explain about this topic, I am moving on to the next one. However, I will do a worked example related to the topic. You can find these examples at the end of the article.
| THE LOGICAL “OR” OPERATOR
In the example above we saw the AND operation in detail. As understood here, the “|” operator will subject the numbers to a bit-level logical “or” operation. In that case, let us make our own inference before writing code. Then let us try to verify it with the program. Again let us process the numbers 10 and 7 with the “|” operator; how the operation is done was shown in detail in the previous topic.
♦ (n = 0) “0” for 10 | “1” for 7 => 0|1 = 1 is obtained.
♦ (n = 1) “1” for 10 | “1” for 7 => 1|1 = 1 is obtained.
♦ (n = 2) “0” for 10 | “1” for 7 => 0|1 = 1 is obtained.
♦ (n = 3) “1” for 10 | “0” for 7 => 1|0 = 1 is obtained.
♦ (n = 4) “0” for 10 | “0” for 7 => 0|0 = 0 is obtained.
There is no need to continue after n = 3. The number obtained is “1111”. The decimal equivalent of this number is “15”, and its hexadecimal equivalent is “f”. Now if we write the necessary code;
// | operatörü - | operators
int number14 = 10;
int number15 = 7;
int number16 = number14 | number15;
Console.WriteLine(
"number14 = " + number14 +
"\nnumber15 = " + number15 +
"\nnumber16 = number14 | number15 = " + number16 +
"\nBitwise number14 = " + Convert.ToString(number14, 2) +
"\nBitwise number15 = " + Convert.ToString(number15, 2) +
"\nBitwise number16 = " + Convert.ToString(number16, 2)
);
Console.WriteLine("****************************************************************************");
The output of these lines is as follows;
number14 = 10
number15 = 7
number16 = number14 | number15 = 15
Bitwise number14 = 1010
Bitwise number15 = 111
Bitwise number16 = 1111
As you can see, we obtained a result entirely in line with our expectations.
Important note: the “or” operation is never ever the addition of numbers! It cannot be likened to it, and indeed should never be likened to it!
^ THE LOGICAL “XOR” OPERATOR
Let us approach this operator the same way we did with the “or” operator. This operator returns 1 if the compared bits are the inverse or complement of each other, and 0 otherwise. In that case, if we again compare the numbers 10 and 7;
♦ (n = 0) “0” for 10 ^ “1” for 7 => 0^1 = 1 is obtained.
♦ (n = 1) “1” for 10 ^ “1” for 7 => 1^1 = 0 is obtained.
♦ (n = 2) “0” for 10 ^ “1” for 7 => 0^1 = 1 is obtained.
♦ (n = 3) “1” for 10 ^ “0” for 7 => 1^0 = 1 is obtained.
♦ (n = 4) “0” for 10 ^ “0” for 7 => 0^0 = 0 is obtained.
The number we obtained is “1101”. The decimal equivalent of this number is “13”. Let us examine it by writing the necessary code;
// ^ operatörü - ^ operators
int number17 = 10;
int number18 = 7;
int number19 = number14 ^ number15;
Console.WriteLine(
"number17 = " + number17 +
"\nnumber18 = " + number18 +
"\nnumber19 = number17 ^ number18 = " + number19 +
"\nBitwise number17 = " + Convert.ToString(number17, 2) +
"\nBitwise number18 = " + Convert.ToString(number18, 2) +
"\nBitwise number19 = " + Convert.ToString(number19, 2)
);
Console.WriteLine("****************************************************************************");
If we look at the output of these lines;
number17 = 10
number18 = 7
number19 = number17 ^ number18 = 13
Bitwise number17 = 1010
Bitwise number18 = 111
Bitwise number19 = 1101
LOGICAL OPERATORS ON SIGNED OR NEGATIVE INTEGERS
Let us try all of the examples we did above with negative numbers too. In fact, let us try one negative and one positive number and examine the results. I will keep the examples here short, because about six examples would need to be done here. I will do one and first obtain a result. Then, by interpreting that result, I will make an inference. I will test my inference with an example. After these operations, you can try the four cases you are curious about yourself. In fact, you should. Now let us compare 10 and -7 by writing a program with the “|” operator and interpret the result.
int number20 = 10;
int number21 = -7;
int number22 = number20 | number21;
Console.WriteLine(
"number20 = " + number20 +
"\nnumber21 = " + number21 +
"\nnumber22 = number20 | number21 = " + number22 +
"\nBitwise number20 = " + Convert.ToString(number20, 2) +
"\nBitwise number21 = " + Convert.ToString(number21, 2) +
"\nBitwise number22 = " + Convert.ToString(number22, 2)
);
Console.WriteLine("****************************************************************************");
The output of these lines is as follows;
number20 = 10
number21 = -7
number22 = number20 | number21 = -5
Bitwise number20 = 1010
Bitwise number21 = 11111111111111111111111111111001
Bitwise number22 = 11111111111111111111111111111011
As you can see, the 32nd bit, which is the sign bit, is subjected to this operation in the same way. That is, whether our numbers are signed or not, each bit is processed and no special rule is applied.
In that case, let us ask what the ^ operator does. With positive integers we had obtained the value 13. A voice inside me says we will obtain -13. Let us first see;
// negatif sayılar ile ^ operatörü - ^ operators with negative numbers
int number23 = 10;
int number24 = -7;
int number25 = number20 ^ number21;
Console.WriteLine(
"number23 = " + number23 +
"\nnumber24 = " + number24 +
"\nnumber25 = number23 ^ number24 = " + number25 +
"\nBitwise number23 = " + Convert.ToString(number23, 2) +
"\nBitwise number24 = " + Convert.ToString(number24, 2) +
"\nBitwise number25 = " + Convert.ToString(number25, 2)
);
Console.WriteLine("****************************************************************************");
The output of these lines is as follows;
number23 = 10
number24 = -7
number25 = number23 ^ number24 = -13
Bitwise number23 = 1010
Bitwise number24 = 11111111111111111111111111111001
Bitwise number25 = 11111111111111111111111111110011
It came out entirely in line with my expectation. You may be a little confused here. If we rearrange the output above;
number23 = 10
number24 = -7
number25 = number23 ^ number24 = -13
Bitwise number23 = 00000000000000000000000000001010
Bitwise number24 = 11111111111111111111111111111001
Bitwise number25 = 11111111111111111111111111110011
Again it is obvious that each bit is processed within itself. However, the fact that -7 is 11111111111111111111111111111001, and therefore that the obtained result is 11111111111111111111111111110011, may be confusing. As I said, I will not answer in this article the question of why the bits of negative numbers that should be 0 are 1. Perhaps in another article. But if you accept this for now and apply the operations, you can predict the result of the other four cases quite comfortably.
EXAMPLES AND THEIR SOLUTIONS
Note: Although we do not need it in these solutions, it is useful to give this information. These operators have an operator-precedence order. This order, starting from the highest priority, is as follows; ♦ ~
♦ << , >> and >>>
♦ &
♦ ^
♦ |
EXAMPLE 1: ODD OR EVEN?
Even people who have had software training or have merely glanced at the world of software have done this example. Let us approach it a little differently.
// TEK Mİ ÇİFT Mİ? - ODD OR EVEN?
Console.WriteLine("Lütfen bir sayı giriniz - Please enter a number");
int number26 = Convert.ToInt32(Console.ReadLine());
int number27 = number26 & 1;
if (number27 == 1)
{
Console.WriteLine("tek - odd");
}
else
{
Console.WriteLine("çift - even");
}
Console.WriteLine("****************************************************************************");
Let us give the number 10 to this program as input. The output is as follows;
Lütfen bir sayı giriniz — Please enter a number
10
çift — even
This time let us enter the number 7. The output is as follows;
Lütfen bir sayı giriniz — Please enter a number
7
tek — odd
Now it is worth explaining a little of what happens here.
First, to determine whether a number is odd or even, we used to divide it by 2 and look at the remainder.
If the remainder is 0, it is even. If it is 1, it is odd. For example;
// TEK Mİ ÇİFT Mİ? - ODD OR EVEN?
Console.WriteLine("Lütfen bir sayı giriniz - Please enter a number");
int number26 = Convert.ToInt32(Console.ReadLine());
int number27 = number26%2; // ← ← ←
if (number27 == 1)
{
Console.WriteLine("tek - odd");
}
else
{
Console.WriteLine("çift - even");
}
Console.WriteLine("****************************************************************************");
However, if you examine a number in the binary system, you can easily see the following. Let us examine the base-conversion operation above;
(1*(2³)) + (0*(2²)) + (0*(2¹)) + (1*(2⁰)) = 8 + 0 + 0 + 1 = 9
If you pay attention to this example, all the places are multiplied by powers of 2 and then summed. But there is a point I want to draw your attention to here. If you examine the example carefully, you can see the following very clearly;
in the binary system a place can be either 0 or 1. 0 is even and 1 is odd. I do not want to get into the debate of whether 0 is even. For now, just accept it this way. Now, if you look carefully, when converting a binary number to decimal, each place is multiplied by 2 and its powers. This means that, as a result of all the operations inside the parentheses above, even numbers will be obtained. Except for a single parenthesis or a single place. That place is the one whose place value is 0 — the 1st place, or bit, or the least significant bit. We know that if the exponent of any number is 0, the result is 1. Of course, 2⁰ is likewise 1. In that case, the result coming from this place will be either 0 or 1. From all the remaining places, even numbers will come. The least significant bit determines whether the number resulting from this addition is odd or even.
In that case, when we write a program, when finding whether a number is even or odd, looking only at the least significant bit will give us the correct result. But how;
Let us examine the program above step by step. Suppose we entered the value 10. The binary equivalent of the number 10 is 1010. In the second step we put this number through an AND operation with the number 1. As a result of this operation, the following is obtained;
(n = 0) for the number 10 “0” & for the number 1 “1” => 0 & 1 = 0 is obtained.
(n = 1) for the number 10 “1” & for the number 1 “0” => 0 & 0 = 0 is obtained.
(n = 2) for the number 10 “0” & for the number 1 “0” => 0 & 0 = 0 is obtained.
(n = 3) for the number 10 “1” & for the number 1 “0” => 1 & 0 = 0 is obtained.
(n = 4) for the number 10 “0” & for the number 1 “0” => 0 & 0 = 0 is obtained.
As you will see, the number obtained as a result of this operation is 0. Then, during comparison, as a result of the 0 == 1 check, the if block is not entered, and inside the else block the number is said to be even.
Please do the same operation for the number 7 yourself. You will see that the result comes out as 1.
So, is there a benefit to using a bitwise operator instead of % (that is, mod) for this operation? Yes, there is! However, in any program that star developers like me, or those still in training, will write, this degree of optimization is not needed. This operation is quite fast compared to using mod. As you can guess, first a division will be performed, and that operation will take longer than the operation we did. But, as I said, in our programs this speed difference is insignificant.
I will recommend that you write a program where you can see this speed difference.
EXAMPLE 2: FINDING THE 30-DAY MONTHS
My dear teacher Fatih Kaan Açıkgöz gave us a problem during our training. While solving this problem, I needed to calculate the duration between two dates. But I wanted to calculate this duration between the two dates not by using the “DateTime” class, but with an algorithm I would write myself. Honestly, I did it and I succeeded. However, there was an important problem! The program, which calculated short durations like 1 or 2 years quickly enough, took about 15 minutes to calculate the day difference of a 15-year period — not 100 or 200 — when the difference between the dates was substantial. For that reason I gave up and deleted it.
But now that I think about it, maybe you can write this algorithm. I have no intention of dealing with it! But I will give you an important hint about where you should start in the program you will write. Now let us begin writing our program. First, we need an Enum. Here there is only one rule you need to pay attention to when giving values to the elements.
[Flags]
public enum Aylar_Months
{
Hiçbiri_None = 0,
Ocak_January = 1,
Şubat_February = 2,
Mart_March = 4,
Nisan_April = 8,
Mayıs_May = 16,
Haziran_June = 32,
Temmuz_July = 64,
Ağustos_August = 128,
Eylül_September = 256,
Ekim_October = 512,
Kasım_November = 1024,
Aralık_December = 2048
}
You have probably understood what the rule is. For example, the value is 1 for January, 10 for February. Think about where I am going with 100 for March, 1000 for April.
By the way, you can examine the Flags attribute yourself. It is a fun topic. I will not go into its details, but we are just saying that we will treat this enum as a bit field, that’s all.
Now let us write a program that lists only the months that have 30 days;
// 30 GÜNLÜK AYLARI BULMA - FIND 30 DAY MONTHS
Aylar_Months ay_Month = Aylar_Months.Ocak_January | Aylar_Months.Mart_March | Aylar_Months.Mayıs_May | Aylar_Months.Temmuz_July | Aylar_Months.Ağustos_August | Aylar_Months.Ekim_October | Aylar_Months.Aralık_December;
Console.WriteLine("30 gün çeken aylar - Months with 30 days:");
foreach (Aylar_Months bit in Enum.GetValues(typeof(Aylar_Months)))
{
if (bit == Aylar_Months.Hiçbiri_None)
continue;
if ((ay_Month & bit) != 0)
{
Console.WriteLine(bit.ToString());
}
} }
}
The output of these lines is as follows;
30 gün çeken aylar — Months with 30 days:
Ocak_January
Mart_March
Mayıs_May
Temmuz_July
Ağustos_August
Ekim_October
Aralık_December
As you can see, this is quite different from the program you would write by guesswork without knowing these operations. But at its core it contains the same operations. Only, because we operate at the bit level, it runs much faster. In this way we can write an enormous class. This class that has been written will be far higher-performing than my slow algorithm.
Let me explain the algorithm a little. Then please examine this program carefully. First, let us look at the numeric value of the “ay_Month” variable. Based on what I explained above, as a result of this operation the numeric value of the “ay_Month” variable is; 1010101010101, decimal 5461 and hexadecimal 1557.
Ocak_January: 0000 0000 0000 0000 0000 0000 0000 0001
Mart_March: 0000 0000 0000 0000 0000 0000 0000 0100
Mayıs_May: 0000 0000 0000 0000 0000 0000 0001 0000
Temmuz_July: 0000 0000 0000 0000 0000 0000 0100 0000
Ağustos_August: 0000 0000 0000 0000 0000 0001 0000 0000
Ekim_October: 0000 0000 0000 0000 0000 0100 0000 0000
Aralık_December: 0000 0000 0000 0000 0001 0000 0000 0000
Now let us look at the operation the logical operator performs with the full list; as an example let us pick January.
If the bit value is currently in January, the following operation is performed.
(1010101010101 & 00000000000001) != 0
(00000000000001) != 0 true!
In that case January has 30 days. So what about February.
Let us also look at this case quickly;
(1010101010101 & 00000000000010) != 0
(00000000000000) != 0 false!
As you can see, our algorithm works quite properly. Please examine the program step by step in this way and try to understand it. That is all from me for now. Until we meet in other articles. Work hard and stay well.
REFERENCES
Bitwise and shift operators (C# reference). MICROSOFT 02/08/2023
Bit WIKIPEDIA 04/08/2023
To access all the code in the article, you can check out my github page
