1. Boolean Logic

In English, complicated conditions can be formed using the words "and", "or", and "not." For example, "If there is a test and you did not study for it, or you studied for it and you used the wrong notes, you are in trouble." "And", "or", and "not" are boolean operators, and they exist in C# as well as in English. In this section, we'll delve into how you can use boolean operators to give if and while statements more decision-making power.

1.1. Making Decisions with Boolean Values

Recall that the only values in the bool data type are true and false. To this point, we haven't used boolean values explicitly for much other than situations where we needed a simple flag variable. I say "explicitly," because you've been using boolean values quite a bit, perhaps without realizing it. Every time you write an if or a while statement, boolean values are involved. Let me explain.

When I first introduced if statements, I said you had to use a comparison to determine which branch to take, like this:

if (value1 relational-op value2) { ... }

The result of a comparison like this is a boolean value. In other words, the relational operators perform a comparison between the two values, and produce a boolean result. When you write

if (x < 5) { ... }

the < operator checks to see if x is less than 5. If so, it produces the value true; otherwise, it produces the value false. The if statement then examines the boolean result to determine which branch it should execute.

Now, it's time to let you in on a little secret. You don't necessarily have to write a comparison to produce a boolean value for an if statement. All you have to do is write an expression that produces a boolean value. For example, if you have a boolean variable done, you might write an if statement like this to check to see if done contains the value true:

if (done == true) { ... }

However, you could also write the if statement like this:

if (done) { ... }

This statement is equivalent to the one that explicitly compares done to true. Because the if statement will accept anything that yields a boolean value, it allows you to use a simple boolean variable in parenthesis. If the variable's value is true, it executes the true statements; otherwise, it executes the false statements (if an else section is present). Using a comparison is unnecessary in this case. Let's look at another example.

In the last chapter, you learned to test to see if a particular character was a letter like this:

if (Char.IsLetter(ch) == true) { ... }

Because the if statement requires a boolean value, you can completely omit the explicit comparison to true, and write this:

if (Char.IsLetter(ch)) { ... }

If ch contains a letter, the IsLetter method returns true, and the if statement executes the true-statements; otherwise, it executes the false-statements.

The key to understanding if statements, then, is to realize that what you really have to supply in parenthesis is a boolean valued expression, like this:

if ( boolean-expression ) { 
  // true statements
} else {
  // false statements
}

Since the relational operators produce a boolean value, writing comparisons with relational operators is a very common way to get the required boolean value, but there are times when a simple comparison is either unnecessary or too limiting. For more expressiveness, we must turn to the boolean operators.

1.2. Boolean Operators

Boolean operators are operators that require boolean-valued operands and produce boolean results. Here are C#'s boolean operators, listed in order of precedence (highest precedence first):

Table 5.1. Boolean Operators

OperatorMeaningExample
!notif (!Char.IsLetter(ch)) { ... }
&&andif (x < 1 && y > 2) { ... }
||orif (x < 2 || y > 3) { ... }

The basic idea of the boolean operators is pretty easy to grasp if you simply substitute the corresponding English word and read the expression. In the example for the && operator above, you might read it "if x is less than 1 and y is greater than 2, do such and such." In order for the if statement to execute the true statements, both conditions have to be true. Let's say x is 0; y is 3. That would make both conditions true, and the && operator would produce the value true. If either condition is false, though, the && operator will produce the value false.

The "or" operator is represented by ||. (That's two of the vertical line characters, |, located on the backslash key on most US keyboards.) The expression "A || B" is true if either A is true or B is true, or if both are true. "A || B" is false only if both A and B are false.

The "not" operator is a unary operator. In C#, it is indicated by ! and is written in front of its single operand. It produces a boolean value that is the opposite of its operand. If the operand's value is true, applying the ! operator yields false; if the value was false, it becomes true. As an example, the following two statements are equivalent:

if (x != 0) { ... }

if ( ! (x == 0) ) { ... }

One common use for the ! operator is to flip the result of a boolean method to reverse the meaning. The following two statements both do the job:

  1. if (Char.IsLetter(ch) == false) { ... }
  2. if (!Char.IsLetter(ch)) { ... }

The second approach is preferred by most C# programmers because it is shorter. Here's how it works: the ! operator takes the boolean value returned by the IsLetter method and flips it. If ch is a letter (IsLetter produced true), the value is flipped to false, and the false statements execute. If ch is not a letter (IsLetter produced false), the value is flipped to true, and the true statements execute. Take a moment to convince yourself that this behavior is identical to what would happen in the first approach.

1.3. Smart Evaluation

The operators && and || are "smart" about how they evaluate their operands. If they can determine a result by looking at the first operand, they don't bother evaluating the second. Specifically, think about the expression (A && B). If A is false, there is no need to check B's value; the result must be false (Remember that && only produces true if both operands are true). Also, if the first operand of an || expression is true, the result must be true, so the second operand is not evaluated in that case, either.

Programmers can take advantage of this behavior to write shorter code. Consider the test

(x != 0) && (y/x > 1)

Suppose that the value of x is in fact zero. In that case, the division y/x is illegal, since division by zero is not allowed. A programmer wishing to avoid this possibility might write the code this way:

if (x != 0) {
  if (y / x > 1) {
    ...
  }
}

This code explicitly guards against the possibility of a divide by zero. However, the following code would work just as well:

if ((x != 0) && (y/x > 1)) {
  ...
}

To understand why this is safe, consider the potentially dangerous case: x = 0. When the && operator evaluates (x != 0), it finds that the result is false, and so it knows that ((x != 0) && anything) has to be false. Therefore, it doesn't bother to evaluate the second operand, (y/x > 1). The division by zero is avoided, and no crash occurs.

Remember that the order of the operands matters here. Consider this dangerous approach:

// DON'T DO IT THIS WAY
if ((y/x > 1) && (x != 0)) {
  ...
}

If x is 0, a runtime crash will occur here, because && and || always evaluate the first operand.

This "smart" evaluation behavior is called "short-circuiting". For the most part, you don't have to think about it much. But at times, it will make your programming life a little easier.

On a technical note: There are actually non-short-circuited versions of && and ||, which are written as & and |. Don't use them unless you have a particular reason to do so.

1.4. Using Boolean Expressions

You're not limited to simple boolean expressions. You can write sophisticated, powerful boolean expressions that combine several boolean operators. Here are some examples of common kinds of boolean expressions, to get you thinking about the possibilities:

ExpressionExplanation
letter == 'A' || letter == 'B' || letter == 'C' || letter == 'D'Yields true if letter is 'A', 'B', 'C', or 'D'
letter != 'A' && letter != 'B' && letter != 'C' && letter != 'D'Yields true if letter is anything other than 'A', 'B', 'C', or 'D'
!(letter == 'A' || letter == 'B' || letter == 'C' || letter == 'D')Yields true if letter is anything other than 'A', 'B', 'C', or 'D'
Char.IsDigit(letter) || Char.IsLetter(letter)Yields true if letter is a letter or a digit (Char.IsDigit and Char.IsLetter each return a boolean result)
letter == 'C' || letter != 'A' && letter != 'B'Yields true if letter is 'C', or something other than 'A' or 'B'

By the way, although many boolean expressions might seem simple to reason about intuitively, don't fall into the trap of trying to translate loosely between English statements and complicated boolean logic. For example, consider this statement:

"If the letter is not an 'A' or a 'B' or a 'C', print an error."

In other words, we want to allow the letters 'A', 'B', and 'C', but not any others. You might reasonably attempt to translate that into C# like this:

if (letter != 'A' || letter != 'B' || letter != 'C') {  print_an_error(); }

Now, think about what will happen when letter is, say, 'D'. The expression would reduce to

true || true || true

which yields true, so the error message would be printed. Great! But what happens when letter is 'A'?

false || true || true

This too yields true, and the error message still gets printed. Hmmm. This isn't what we wanted. What value would letter have to be to avoid the error message? Think about it for a moment.

If you can't think of any values that would make the expression false, good. There aren't any. The if statement is logically equivalent to writing

if (true) { print_an_error(); }

In other words, it's a mistake. The correct way to write the expression is to use the && operator, not the || operator:

if (letter != 'A' && letter != 'B' && letter != 'C') {  print_an_error(); }

Take a few moments to convince yourself that this expression produces the desired result.

To reason your way through a complicated boolean expression, it helps to

  • Fully parenthesize the expression, according to precedence

  • Pick a sample value for each variable in the expression, and work your way through the evaluation, just like the computer would.

Let's work through an example from the table above:

letter == 'C' || letter != 'A' && letter != 'B'

Since || has a lower precedence than &&, the computer would perform the operations as if the expression were parenthesized this way:

(letter == 'C') || ((letter != 'A') && (letter != 'B'))

Now, let's pick a sample value to experiment. Let's say letter is 'F'. Here's how the computer would evaluate the expression, step by step:

  1. (letter == 'C') -> false. This leaves (false || ((letter != 'A') && (letter != 'B')))

  2. (letter != 'A') -> true. This leaves (false || (true && (letter != 'B')))

  3. (letter != 'B') -> true. This leaves (false || (true && true))

  4. (true && true) -> true. This leaves (false || true)

  5. (false || true) -> true.

Thus, if letter is 'F', the expression would produce true, which is what we would expect from the intuitive meaning given in the table.

1.5. Putting Numbers in Order

As an example of using boolean operators, lets suppose that x, y, and z are variables of type int, and that each variable has already been assigned a value. Consider the problem of printing out the values of the three variables in increasing order. For examples, if the values are 42, 17, and 20, then the output should be in the order 17, 20, 42.

One way to approach this is to ask, "which order should x and y be printed in?" Once that's known, you have to decide where to stick in z.

where does x belong in the list? It comes first if it's less than both y and z. It comes last if it's greater than both y and z. Otherwise, it comes in the middle. We can express this with a 3-way if statement, but we still have to worry about the order in which y and z should be printed. In pseudocode,

if (x < y && x < z) {
   output x, followed by y and z in their correct order
} else if (x > y && x > z) {
   output y and z in their correct order, followed by x
} else {
   output x in between y and z in their correct order
}

Determining the relative order of y and z requires another if statement, so this becomes

if (x < y && x < z) {        // x comes first
   if (y < z)
      Console.WriteLine( x + " " + y + " " + z );
   else
      Console.WriteLine( x + " " + z + " " + y );
} else if (x > y && x > z) {   // x comes last
   if (y < z)
      Console.WriteLine( y + " " + z + " " + x );
   else
      Console.WriteLine( z + " " + y + " " + x );
} else {                       // x in the middle
   if (y < z)
      Console.WriteLine( y + " " + x + " " + z);
   else
      Console.WriteLine( z + " " + x + " " + y);
}

You might check that this code will work correctly even if some of the values are the same. If the values of two variables are the same, it doesn't matter which order you print them in.

Note, by the way, that even though you can say in English "if x is less than y and z,", you can't say in C# "if (x < y && z)". The && operator can only be used between boolean values, so you have to make separate tests, x<y and x<z, and then combine the two tests with &&.

There is an alternative approach to this problem that begins by asking, "which order should x and y be printed in?" Once that's known, you only have to decide where to stick in z. This line of thought leads to different C# code:

if ( x < y ) {  // x comes before y
   if ( z < x ) 
      Console.WriteLine( z + " " + x + " " + y);
   else if ( z > y )
      Console.WriteLine( x + " " + y + " " + z);
   else
      Console.WriteLine( x + " " + z + " " + y);
} else {          // y comes before x
   if ( z < y ) 
      Console.WriteLine( z + " " + y + " " + x);
   else if ( z > x )
      Console.WriteLine( y + " " + x + " " + z);
   else
      Console.WriteLine( y + " " + z + " " + x);
}

Once again, we see how the same problem can be solved in many different ways. The two approaches to this problem have not exhausted all the possibilities. For example, you might start by testing whether x is greater than y. If so, you could swap their values. Once you've done that, you know that x should be printed before y.