How the boolean logical operators are used and get overloaded in C#

Posted by : on

Category : C#

Introduction - true and false

In C# the true and false represent two different things, they can be either the value of the type bool, or an operator that returns a boolean value for a specific type. Both can be used in boolean expressions and eventually a value of type bool is calculated that changes the flow of control of our program through one of the following statements:

  • The if statement
  • The while statement
  • The do statement
  • The for statement

The boolean expression in these statements, is calculated either by an implicit conversion of our type to bool, or by using the true and false operators if the conversion isn’t possible.

The boolean logical operators are four: AND & , OR |, Exclusive OR (XOR) ^ and the unary logical negation !. The conditional logical operators which are also known as short circuit operators are two: AND && and OR ||

Short circuit operators

The conditional operators are usually used in place of their logical counterparts because of their speed. The reason is that when the result of a boolean expression is known without the need to calculate the whole expression, the short circuit operators don’t do all the calculations.

This can happen in two cases:

  • In an expression where the operation is an AND operation, if the first boolean value is not false, only then the second value is calculated.
  • In an expression where the operation is an OR operation, if the first boolean value is not true, only then the second value is calculated.

Mind that the above rules are different from:

  • if the first boolean value is true, only then the second value is calculated
  • if the first boolean value is false, only then the second value is calculated

The reason is that the boolean values true and false for the operations true and false, are not guaranteed to complement each other. An operation for a type can return both false for the true operation and false for the false operation, as we will see later in this post.

Let’s see a code example:

public class Bar
{
   public bool Compare(int a, int b) => a > b;
}

Bar bar = new();

Console.WriteLine(false && bar.Compare(1,2));
Console.WriteLine(true || bar.Compare(1,2));

In both of those cases the bar.Compare(1,2) method will never be executed.

Usually that’s desirable, but we have to keep in mind that there are cases where one method not only performs logic in our data, but also changes our data. Most often than not, this is bad programming, as it disobeys one important rule of OOP: A method should only perform operations in its data, or change its data but not both, or more restrictively: A method should only be doing one thing.

Sometimes though we may find ourselves doing the following:

Dictionary<int, string> foo = new()
{
   { 1, "one" },
   { 2, "two" },
   { 3, "three" }
};

if (bar.Calculate(1,2) || foo.TryGetValue(1, out var aString))
{
   Console.WriteLine(aString); // Compiler error
}

In this case the compiler will have an error, because it can see that there are cases where the foo.TryGetValue(1, out var aString) won’t be executed. To fix that, we just have to reverse the order of the operations:

if (foo.TryGetValue(1, out var aString) || bar.Calculate(1,2))
{
   Console.WriteLine(aString);
}

but in a case like the following, using the | logical operator is the only solution, if for some reason we don’t want to cache the results of our operations:

if (foo.TryGetValue(1, out var aString) || foo.TryGetValue(2, out var aSecondString))
{
   Console.WriteLine(aString, aSecondString);
}

The compiler won’t always see an error if we don’t follow clean code practices, and that may lead to difficult to find bugs, for example:

Enemy firstEnemy = new();
Enemy secondEnemy = new();

if (firstEnemy.Attack(ourPlayer) || secondEnemy.Attack(ourPlayer))
   ShowAWarningTo(ourPlayer);

public class Enemy
{
   private int _attackRange = 10;
   
   public bool Attack(Player player)
   {
      if (CalculateDistance(this, player) < _attackRange)
      {
         player.Hit();
         return true;
      }
     
      return false;
   }
}

Here even if both enemies are able to hit our player, the second enemy never will, because as soon as the firstEnemy.Attack returns true, the secondEnemy.Attack will never be executed.

This gets solved by using the | operator, or even better: We don't design methods that do two things. In this case, check if an Attack can be done and also perform it.

Note: Boolean operators and bitwise operators

The & and | operators perform logical operations when used with booleans, but also perform bitwise operations when used with integer values. We have to be careful when we use bitwise operations to reach boolean conclusions in C#, as the bitwise operations don’t align with the boolean operations.

In C# a false value is a value that has all its bits zero, but a true value is any value that is not false. In other languages the true may be represented with the value of 1, or the true can have a value of 1 and the false the value of -1.

This can lead to problems with low level code, if we expect the bitwise operations for integers to be aligned with the boolean operations:

int x = 1;
int y = 2;
int z = 5;
bool xBool = Convert.ToBoolean(x);
bool yBool = Convert.ToBoolean(y);
bool zBool = Convert.ToBoolean(z);

Console.WriteLine(Convert.ToBoolean(x)); // True
Console.WriteLine(Convert.ToBoolean(y)); // True
Console.WriteLine(Convert.ToBoolean(z)); // True

Console.WriteLine(xBool && zBool); // True
Console.WriteLine(Convert.ToBoolean(x&z)); // True

Console.WriteLine(xBool && yBool); // True
Console.WriteLine(Convert.ToBoolean(x&y)); // False !!!

Compound assignments

The &, | and ^ operators support the compound assignments:

  • &=
  • |=
  • ^=

that work the same way other compound assignments work. The && and || operators don’t support compound assignments.

Overloading the logical operators and the true and false operators

The logical operators can be overloaded, the same way every other operator can, for example:

Foo foo = new(2);
Console.WriteLine((foo & foo).Value);

public class Foo
{
   public int Value;
   public Foo(int value)
   {
      Value = value;
   }
   public static Foo operator &(Foo x, Foo y) => new Foo(x.Value + y.Value);
}

Now the AND operator between two foo objects returns a new object of type foo that has been initialized with the Value 4.

The short circuit operators && and ||, cannot be overloaded explicitly, but we can change their behavior by overloading the true and false operators.

The && operator is evaluated like this: T.false(x) ? x : T.&(x, y) where T is a type that has overloaded the operator and x,y objects of that type. From this, we see that if the false operator returns true, then the T.&(x, y) isn’t evaluated.

The || operator is evaluated like this : T.true(x) ? x : T.|(x, y). The same applies here: If the true operator returns true then the T.|(x, y) statement isn’t evaluated.

So if we want to change the behavior of those operators, we should overload the true and false operators. We cannot overload only one, both must be always overloaded or none.

The true and false operators don’t have to complement each other, for example the following is perfectly legal:

public class Distance
{
   private readonly int _meters;
   public Distance(int meters) => _meters = meters;

   public static bool operator true(Distance allowed) => allowed._meters > 0;
   public static bool operator false(Distance disallowed) => disallowed._meters < 0;
}

Here if the _meters value is 0, both the true and the false operations will return a false value.

By adding an overload of the & operator we can have this:

public class Distance
{
   private readonly int _meters;
   public Distance(int meters) => _meters = meters;

   public static bool operator true(Distance allowed) => allowed._meters > 0;
   public static bool operator false(Distance disallowed) => disallowed._meters < 0;
   public static Distance operator &(Distance one, Distance two) => new Distance(one._meters + two._meters);
}

but we have to be careful, the above code has a bug. If we run:

Distance firstDistance = new(3);
Distance secondDistance = new(-2);

if(firstDistance) 
   Console.WriteLine("first distance is positive");
else
   Console.WriteLine("first distance is negative");

if(secondDistance) 
   Console.WriteLine("second distance is positive");
else
   Console.WriteLine("second distance is negative");

if(firstDistance & secondDistance)
   Console.WriteLine("sum of distances is positive");
else
   Console.WriteLine("sum of distances is negative");

if(firstDistance && secondDistance)
   Console.WriteLine("sum of distances is positive");
else
   Console.WriteLine("sum of distances is negative");

if(secondDistance && firstDistance)
   Console.WriteLine("sum of distances is positive");
else
   Console.WriteLine("sum of distances is negative");

the result will be:

first distance is positive
second distance is negative
sum of distances is positive
sum of distances is positive
sum of distances is negative <-- bug!

The reason is described above: The && calculation will evaluate in the last if statement: the secondDistance and because it is false, won’t apply the operation with the & operator and will return false immediately.

Overloading the implicit operator

From the above, overloading the true and false operators is an edge case. Usually we can accomplish the desired result by overloading the implicit bool operator for logical operations in our types and the relevant operator for arithmetic evaluations.

public class Distance
{
   private readonly int _meters;
   public Distance(int meters) => _meters = meters;

   public static implicit operator bool(Distance distance) => distance._meters > 0;
   public static Distance operator +(Distance one, Distance two) => new(one._meters + two._meters);
}
if(firstDistance) 
   Console.WriteLine("first distance is positive");
else
   Console.WriteLine("first distance is negative");

if(secondDistance) 
   Console.WriteLine("second distance is positive");
else
   Console.WriteLine("second distance is negative");

if(firstDistance + secondDistance)
   Console.WriteLine("sum of distances is positive");
else
   Console.WriteLine("sum of distances is negative");

if(secondDistance + firstDistance)
   Console.WriteLine("sum of distances is positive");
else
   Console.WriteLine("sum of distances is negative");

result:

first distance is positive
second distance is negative
sum of distances is positive
sum of distances is positive

There are some cases that overloading the & operator, but also the true and false operators makes sense, but usually if we need logic in one of our types that can support three values, using a nullable boolean bool? is preferable.

Nullable types and logical operators

The nullable value types in C# support the same operators as their non nullable counterparts. These are called lifted operators and their behavior is to return null if one or both of their operands is null.

With the exception of the logical operators.

For the logical operators the & produces true if both operands are true, false if at least one is false and null otherwise.

The | operator, produces false if both operands are false, true if ate least one is true and null otherwise.

The short circuit operations, are not supported for nullable booleans.

You can check the table here: Nullable boolean logic table

Conclusion

That’s it for the conditional operators, I hope you found this post useful. Thank you for reading and as always, if you have any questions or comments you can use the comments section or contact me directly via the contact form or by email, also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.


Follow me: