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.
Projects and Solutions
Create a new project called MakingDecisions. HOWEVER, this time, change the solution name to ITSE1330Solution-1. We are taking this approach so we can add multiple projects to one solution container. This is not necessary to accomplish our work over the next few chapters. I am only covering it to demonstrate that a solution can contain multiple projects. And, I personally find it convenient to store multiple related projects within one solution. When that solution is opened, all projects within that solution conveniently appear in the Solutions Explorer. Note: For your C# program assignments submitted via Blackboard, include only one project per solution. Again, for your program assignments, do NOT combine multiple projects into one solution for submission.
Make a copy of the Operators solution to be safe before proceeding. Reminder: backup frequently and perform test restorations to ensure your backups are correct. Now, as an aside exercise, let's add the Operators project to the ITSE1330Solution-1 solution. In the solution explorer, R-Click the solution (not the project) | Add | Existing Project... and navigate to the location of the Operators.csproj file. Notice below that Operators is now part of the ITSE1330Solution-1. Be careful though, this is the exact same project as that in the Operators solution. A change made in either location will be realized in both solutions.
The reference to the existing Operators project can be seen by inspecting the ITSE1330Solution-1.sln file. I'm using Notepad++ to view the file contents. Notice on line 8 that the project is still located in the Operators solution directory. If we wanted a copy, we could have copied the Operators project folder (one level down from the Operators solution directory) and then added the project to new our destination solution named ITSE1330Solution-1.
Remove the Operators project from the ITSE1330Solution-1 by R-clicking on the project and selecting Remove. This only removes the reference from within the ITSE1330Solution-1 (specifically the project listing is removed from within the .sln file). However, lines 8-9 remain in the .sln until the file is saved. Close the solution to see the dialog and select Yes.
Now view the .sln file and observe that the Operators project listing has been removed.
Let's proceed with the MakingDecisions project. Code the Program.cs file as shown. There are a few new concepts to cover before introducing the decision structures. On line 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 11 did not have a value assigned, the value would be 32 + 1 = 33. The enum values are accessed using the member access operator as shown on lines 17 and 25 and others.
On line 17 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 14 and 15. The values "AbandonHope" and "AllIsLost" 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 21 the Tryparse() method of the int class is used to convert input read (via ReadLine()) from the console (command line) 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. 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 21 reads a line of string input from the command line, 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 25 is asking "if it is true that myTemp1 is equal to the enum SuperCold, then do the statements in the block on lines 26-28. Otherwise, skip that block." More single if examples are shown on lines 30, 35, and 41. On line 30 myTemp2 is cast since it is of type Temps (see line 17). On line 35 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 41 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 48-55 depict the next type of decision statement which is the "if-else". If the expression being tested is true, line 50 will execute. Otherwise, the else clause (lines 53-55) will run. This is a case of "mutual exclusivity" which means that either line 50 or line 54 will execute but not both. You can see from the output that myTemp1 is either Cold or it is not. Line 50 will always run if line 48 evaluates to true. The else clause will always execute if line 48 is false.
If - Else - If Statements
The if - else statement can be extended indefinitely by chaining multiple if-else-if clauses together. Lines 58-73 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 58 will be false and line the condition on line 62 will be tested which will evaluate to true.
Line 64 will write "myTemp1 is Hot" to the console and execution will then jump to line 76, 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.
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 76-99 demonstrate a variety of arrangements. We start by testing if stringVar == JoeBob. If so, line 78 is output and another test is performed on line 79. However, if stringVar != JoeBob, execution"falls" to line 96, line 98 is output, and the if statement is complete. If stringVar == JoeBob, the next test is line 79 which asks if myTemp1 == (int)Temps.Hot? If so, line 81 is output and the next question on line 82 is posed and so on. Note that if line 84 is output, that means that all three conditions previously tested on lines 76, 79, and 82 were all true.
A similar test could be performed using two && within one if statement. For example, line 35 could be rewritten as two if statements instead of using the &&. However, in our nested example, we want to output line 78 if line 76 is true even if line 79 is not. So, nesting provides extra capabilities beyond multiple &&s that may be useful. One more point, the else on line 91 is known as a "trailing else" which is executed in the event line 76 and line 87 are both false - like a default clause.
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 102-121 include a switch example.
The keyword "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/Inhumane, 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 105 will execute followed by the break. When a break statement is encountered, the switch is exited at that point and execution resumes on line 123. Note that if myTemp2 is equal to either Hot or Inhumane, lines 115-117 will execute.
Unlike C, C++, and Java, the C# switch statement does not support "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 113 and 114. The "no fall through" rule prevents a common type of error in languages that support fall through in which break statements are accidentally omitted.
A sample output of the MakingDecisions project is shown with 110 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.