Recall an earlier description of the three fundamental constructs of programming: sequence, decision, and iteration. In this chapter we consider the various ways C# can help us make decisions.
Coding Multiple Decision Constructs
Let's proceed with the MakingDecisions project. Code the Program.cs file as shown. There is a substantial amount of text below required to explain the code. I recommend that you review the code first, line-by-line to see how much you understand. And then, read through the text explanations and reference the code by the line numbers provided. Also, this is a situation that multiple monitors are extremly beneficial - code on the left and text explanation on the right (or vice-versa).
There are a few new concepts to cover before introducing the decision structures. On lines 10-11 an enum type is used. An enum, short for enumeration, provides a type for a list of named constants. As constants, the values cannot be changed after being assigned in the enum. The values are int types by default but can be any integer type except char.
If no values are supplied for the enum entries, the first will be 0, the next 1, etc. If an entry is not assigned a value but the previous entry has a value, the value of the following entry will be the predecessor incremented by 1. For example, if the entry JustRight on line 10 did not have a value assigned, the value would be 32 + 1 = 33. The enum values are accessed using the member access operator (dot operator) as shown on lines 22 and 35 and others.
On line 22 a new variable named myTemp2 is created of type Temps and is initialized with the value of Temps.Hot which is 90. As we will see below, an explicit cast is required when Temps are compared to integers. This is due to values such as SuperCold and JustRight are of type Temps and not integers so the conversion is necessary.
The next new concept is shown on lines 19 and 20. The values AbandonHopeCold and AllIsLostHot are created as constants using the
const keyword. Constants are beneficial because they protect values from change since they cannot be modified after the initial assignment. Constants are also known as named constants to differentiate between numeric, string, or character literal constants which are literally supplied such as 233, "Hello there", and 'J'. Constants are also helpful because they provide one point for updates and they can help reduce typo errors since intellisense supplies name suggestions. Invalid constant names are flagged as errors but an invalid numerical setting could easily slip through unnoticed. Some languages recommend using all caps for constants but .NET recommends camel case.
On line 28 the Tryparse() method of the int class is used to convert input read (via ReadLine()) from the keyboard into an int. If successful, TryParse() returns true and assigns the converted value to the next argument, in this case myTemp1. Using the keyword
out allows the myTemp1 variable to be passed to the TryParse() method and changed by the method. An out argument does not require initialization before use. Out is similar to the ref keyword but ref requires the variable to be initialized before it is used as an argument. More on out and ref when we review functions. In total, line 28 reads a line of string input from the keyboard, converts it to integer, and assigns that value to the variable myTemp1. The TryParse() method also exists for other types such as double, datetime, bool, and char. We will use the TryParse() method in subsequent chapters to not only convert but to also check for data validity.
The first and most basic structure we consider is the if statement. If statements provide a way to ask questions. The questions can take the form of a single boolean variable or can be more involved expressions. Irrespective of the complexity, if statements are asking the same question, if (true). So, the if statement on line 35 is asking "if it is true that myTemp1 is equal to the enum SuperCold, then do the statements in the block on lines 36-38. Otherwise, skip that block." More single if examples are shown on lines 40, 45, and 50. On line 35 myTemp2 is cast to an (int) since it is of type Temps (see line 22). On line 45 an AND operator && is used to test that two conditions are true. Notice that && is being used to test that myTemp1 is within a range. On line 50 an OR operator || is used to test that one or both of the conditions is true. In this case, both conditions cannot be true. Notice that in this example || is being used to check if myTemp1 is outside of a range.
If Else Statements
Lines 56-63 depict the next type of decision statement which is the if else. If the expression being tested is true, line 58 will execute. Otherwise, the else clause (lines 60-63) will run. This is a case of mutual exclusivity which means that either line 58 or line 62 will execute but not both. You can see from the output that myTemp1 is either equal to Temps.Cold or it is not. Line 58 will always run if line 56 evaluates to true. The else clause will always execute if line 56 is false.
If Else If Statements
The if Else statement can be extended indefinitely by chaining multiple if else if clauses together. Lines 66-85 show multiple if else if statements. As soon as the first condition evaluates to true, the statements within that block are executed and then execution jumps to the next line after the last statement. For instance, if myTemp1 == 90, the condition on line 66 will be false and the condition on line 70 will be tested which will evaluate to true since 90 == Temps.Hot.
Line 72 will write myTemp1 is Hot to the console and execution will then jump to line 88, the next executable line. With symmetrical if else if statements like shown, logically, the conditions within the if else if statements should be mutually exclusive since at most one statement block will be run. Also, if possible, the most common conditions should be tested first to enhance performance by reducing unnecessary conditional testing. One more point, the else on line 82 is known as a trailing else which is executed in the event all of the conditions above are false. Trailing elses are commonly used for default statements and/or to catch errors if none of the above are true.
Nesting Decision Statements
The last if statement type we will consider is the nested version. With nested conditions, any combination of if else if within if else if is allowed. If your conditions become complex, be sure to carefully and thoroughly test to ensure proper operation. Unfortunately, it is all too easy to believe the conditions are correct only to have flaws in the logic.
Lines 88-111 demonstrate a variety of arrangements. We start by testing if stringVar == JoeBob. If so, line 90 is output and another test is performed on line 91. However, if stringVar != JoeBob, executionfalls to line 108, line 110 is output, and the if statement is complete. If stringVar == JoeBob, the next test is line 91 which asks if myTemp1 == (int)Temps.Hot? If so, line 93 is output and the next question on line 94 is posed and so on. Note that if line 96 is output, that means all three conditions previously tested on lines 88, 91, and 94 were all true.
A similar test could be performed using two && within one if statement. For example, line 45 could be rewritten as two if statements instead of using the &&. However, in our nested example, we want to output line 90 if line 88 is true even if line 91 is not. So, nesting provides extra capabilities beyond multiple &&s that may be useful.
An infinite number of logical challenges can be addressed using a combination of if and else if statements. C#, like most modern languages, has another form of decision structure that can be helpful. The switch statement offers an alternative to multiple if else ifs. The benefit of using a switch statement over the if alternatives is that the code of switch tends to be more readable and therefore also less susceptible to incorrect logic. Lines 114-132 include a switch example.
switch begins the statement and myTemp2 is known as the switch expression. A variable of any integer type such as char, short, int, long, and enum is acceptable for a switch expression. The switch expression can also be a string type. The values in the case labels must agree with the type of the switch expression or at least be implicitly convertible to the type of the switch expression. Since myTemp2 is of type Temps, all of the case labels are of type Temps.
A switch section is identified by one or more switch labels or the default label. In the example, there are five switch sections: SuperCold, Cold, JustRight, Hot/InhumaneHot, and default. Each switch section contains a statement list. The WriteLine() methods and break statements comprise the statement lists in the example.
The body of the switch, known as the switch block, is easy to follow. If myTemp2 is SuperCold then the first case will be true and line 117 will execute followed by the break. When a break statement is encountered, the switch is exited at that point and execution resumes on line 134. Note that if myTemp2 is equal to either Hot or InhumaneHot, lines 127-128 will execute.
Unlike C, C++, and Java, the C# switch statement does not support a concept called fall through. That is, break statements (or some form of switch exit) is required for each switch section. However, C# accomplishes fall through like behavior by allowing multiple switch labels per switch section like shown on lines 125 and 126. The no fall through rule prevents a common type of error in languages that support fall through in which break statements are accidentally omitted.
You may notice in the code example below that the vertical lines that connect the braces do not appear. To hide those in VS, deselect the box shown.
A sample output of the MakingDecisions project is shown with 72 entered as the current temperature. Run the code with a variety of inputs and ensure you understand the program flow by comparing the code with your anticipated output and the actual results.
In the next chapter, we begin using the Array class to group items of the same type together for more convenient and efficient processing.