C# Programming · Lesson 5
Getting into the Program Flow
Control C# program execution with if, else, switch, switch expressions, while, do…while, for, break, continue, and scope.
Lesson purpose
Lesson 5 turns a straight-line program into one that can respond and adapt. Until now, every program ran each statement once, top to bottom, and exited. Students learn the three basic kinds of flow control, the if statement, the loop, and the jump, so a program can choose a path, repeat work, and leave a structure early. The lesson also introduces switch expressions, pattern matching, the conditional operator, and scope rules that determine where a variable exists.
Learning objectives
- Explain why decision-making and repetition are essential processor capabilities.
- Write
if,if/else, and nestedifstatements that validate input. - Use a
switchstatement, including shared case labels and adefaultcase, and state its restrictions. - Read and write a switch expression with relational and logical patterns and a discard (
_) arm. - Trace
while,do…while, andforloops, including the initialize, test, body, and update steps. - Use
breakandcontinuecorrectly, including inside nested loops. - Explain block scope for variables declared inside loops and
forheaders. - Avoid hard-coding by naming important limits once.
1. Why programs need flow control
Start with the lesson’s opening program: it prompts for a name, reads it, echoes a greeting, and waits for Enter. It works, but it can only spit back what it was given. Programs become useful when the processor can send execution down one path when a condition is true and another when it is false, and when it can repeat a sequence of instructions.
C# offers three basic kinds of flow control:
| Kind | Keywords | What it does |
|---|---|---|
| Branch | if, else, switch |
Choose one path from a condition or value. |
| Loop | while, do…while, for (and foreach in Lesson 6) |
Repeat a block while a rule allows it. |
| Jump | break, continue, return |
Leave a loop or switch early, or start the next iteration. |
Talking point: no matter which construct is used, a run of the program still follows one ordered path through the statements. The skill to build is tracing that path for a specific input.
Quick check: name a program that would be impossible, or painfully repetitive, without branching or looping.
2. The if statement
The basis of all decision-making in C# is the if statement:
if (bool-expression)
{
// Runs only when the expression is true.
}
// Runs next whether the expression was true or not.
- Boolean test: the parentheses must contain an expression of type
bool, such as a comparison or a logical combination from Lesson 4. - Guarded block: the braces contain the code that runs only when the test is true.
- Join point: after the block, execution continues with the next statement on either path.
decimal principal = 1234m;
if (principal < 0)
{
Console.WriteLine("Principal cannot be negative");
principal = 0;
}
Console.WriteLine($"Principal: {principal}");
C# allows omitting braces when the body is a single statement, but the classroom convention is to always use braces. A later edit that adds a second line cannot then silently fall outside the guarded region.
Validating input: CalculateInterest
The book’s CalculateInterest program reads a principal and an interest rate, checks each, and prints the result:
Console.Write("Enter principal: ");
string principalInput = Console.ReadLine() ?? "";
decimal principal = Convert.ToDecimal(principalInput);
if (principal < 0)
{
Console.WriteLine("Principal cannot be negative");
principal = 0;
}
Console.Write("Enter interest: ");
string interestInput = Console.ReadLine() ?? "";
decimal interest = Convert.ToDecimal(interestInput);
if (interest < 0)
{
Console.WriteLine("Interest cannot be negative");
interest = 0;
}
decimal interestPaid = principal * (interest / 100);
decimal total = principal + interestPaid;
Console.WriteLine();
Console.WriteLine("Principal = " + principal);
Console.WriteLine("Interest = " + interest + "%");
Console.WriteLine("Interest paid = " + interestPaid);
Console.WriteLine("Total = " + total);
Talking points:
Console.Writekeeps the cursor on the prompt line. Tell the user exactly what to enter; an uninformative prompt such as>confuses users.ReadLine()returns text, soConvert.ToDecimal()turns it into a number.ReadLine(),WriteLine(), andToDecimal()are all method calls: each delegates work to a method elsewhere. Methods are covered in Lesson 12.- The rate is divided by 100 because users enter a percentage.
- Sample run: principal 1234 at 21% prints interest paid 259.14 and total 1493.14. With interest -12.5, the program prints the warning, uses 0%, and the total stays 1234.
Indent the statements inside a block. C# ignores indentation, but people rely on it. Visual Studio’s Smart Indenting (Tools ⇒ Options ⇒ Text Editor ⇒ C# ⇒ Tabs) does this automatically; the course uses four spaces.
Prediction: what prints when principal begins at -25?
3. else, avoiding else, and the conditional operator
When two conditions are true opposites, two separate if statements do needless work and can be written so that both run or neither runs:
int max;
if (a > b)
{
max = a;
}
if (a <= b) // redundant second test
{
max = b;
}
The else keyword defines a block that runs when the if block does not, so exactly one path executes:
int max;
if (a > b)
{
max = a;
}
else
{
max = b;
}
Some programmers avoid long else chains by assuming a result and correcting it only when needed:
int max = a;
if (b > a)
{
max = b;
}
The conditional (ternary) operator ?: turns a simple if/else into one expression. The condition comes first; the value after ? is used when it is true, and the value after : when it is false:
bool informal = true;
string name = informal ? "Chuck" : "Charles"; // "Chuck"
Use it sparingly for short, obvious choices; it becomes cryptic quickly. The book also notes that newer C# versions allow a target-typed conditional expression, where the two results can have different types when the destination type can accept both.
Quick check: why is if/else preferable to two tests for a > b versus a <= b? Trace a and b equal.
4. Nesting if statements and avoiding hard-coding
Continuing to ask for an interest rate after the principal is already invalid wastes the user’s time, and some calculations are too expensive to run on bad data. An if statement inside the body of another if is a nested (embedded) statement. Nesting narrows what is known to be true at each level:
const decimal MaximumInterest = 50m;
Console.Write("Enter principal: ");
decimal principal = Convert.ToDecimal(Console.ReadLine());
if (principal < 0)
{
Console.WriteLine("Principal cannot be negative");
}
else
{
Console.Write("Enter interest: ");
decimal interest = Convert.ToDecimal(Console.ReadLine());
if (interest < 0 || interest > MaximumInterest)
{
Console.WriteLine("Interest cannot be negative or greater than " + MaximumInterest);
}
else
{
decimal interestPaid = principal * (interest / 100);
Console.WriteLine("Total = " + (principal + interestPaid));
}
}
- Outer guard: a bad principal ends the conversation before the next prompt.
- Compound validation:
||rejects a value on either side of the valid range. - Deep success path: the calculation runs only when both inputs passed.
- Named boundary: the book declares the maximum as a variable at the top rather than typing
50inside the test. Hard-coding means using values directly in code instead of a named variable or constant. A name documents meaning and lets one edit change every use. The classroom version usesconstbecause the limit never changes.
Sample: principal 1234 and interest 12.5 prints a total of 1388.250.
Quick check: what facts are guaranteed inside the final else?
5. The switch statement
Testing one variable against many values with if/else chains grows tiresome. The switch statement chooses among mutually exclusive cases:
switch (maritalStatus)
{
case 0:
Console.WriteLine("Unmarried");
break;
case 1:
Console.WriteLine("Married");
break;
case 2:
Console.WriteLine("Divorced");
break;
default:
Console.WriteLine("Unknown status");
break;
}
- Switch value: the expression after
switchis evaluated once. - Case labels: each
caseis compared to that value. - Exit:
break(or another exit such asreturn) leaves the switch. - Fallback:
defaulthandles every value no case names; it is often an error path.
The switched value can also be a string, such as switch (s) with case "Davis":.
Restrictions stated in the lesson:
- The switch value traditionally must be a counting (integral) type, including
char, or astring; C# 7.0 and later accept any non-null value. - Each case value must be the same type as the switch value.
- Each case value must be a constant known at compile time;
case x:is legal only whenxis a constant. - Each case section must end with
breakor another exit. Control may not fall through into the next section.
Two labels can share one body by stacking them with no code between them:
switch (s)
{
case "Davis":
case "Hvidsten":
Console.WriteLine("Related families");
break;
case "Smith":
Console.WriteLine("Smith");
break;
default:
Console.WriteLine("Someone else");
break;
}
Quick check: where should an unexpected status value go?
6. Switch expressions and pattern matching
A switch expression (C# 8.0 and later) produces a value instead of running statements. An expression is a construction of operators and operands, such as x > y. The book’s example uses an enumeration, a list of value names (covered fully in Lesson 10):
public enum Greetings { Morning, Afternoon, Evening, Night }
public static string GreetString(Greetings value) => value switch
{
Greetings.Morning => "Good Morning!",
Greetings.Afternoon => "Good Afternoon!",
Greetings.Evening => "See you tomorrow!",
Greetings.Night => "Are you still here?",
_ => "Not sure what time it is!"
};
- The value before
switchis compared against each arm in order. - Each arm reads “pattern
=>result.” _is the discard arm, the default when no other arm matches. Without it, Visual Studio warns that not every value is covered.
The => arrow is the lambda operator. The book gives only a preview: a lambda expression is shorthand for a small unnamed function. For now, read each arm as “if the input is this, produce that.”
Pattern matching (C# 9.0 and later) lets an arm describe a group of values. Relational patterns (>=, <=) combine with and and or:
string category = letter switch
{
>= 'a' and <= 'z' => "lowercase letter",
>= 'A' and <= 'Z' => "uppercase letter",
>= '0' and <= '9' => "number",
>= ' ' and <= '/' or >= ':' and <= '@'
or >= '[' and <= '`' or >= '{' and <= '~' => "special character",
_ => "unknown letter type"
};
With the book’s inputs, 'a' is a lowercase letter, 'B' uppercase, '3' a number, '?' a special character, and 'À' falls to the discard arm. The book’s own version tests '1' through '9', which would misclassify '0'; the classroom version uses '0'.
Modernization note: the book asks students to add a LangVersion entry to the project file for these features. Current .NET templates already use a language version that includes switch expressions and patterns, so that edit is not needed in this course.
Quick check: which arm handles '?', and why?
7. The while loop
A loop executes the same short sequence of instructions repeatedly:
while (bool-expression)
{
// Repeats as long as the expression is true.
}
Every safe loop has four parts:
- Initialize: give the condition the values it needs before the first test.
- Test first: the condition is checked before each pass. If it is false the first time, the body runs zero times.
- Body: one complete iteration of the work.
- Update: change state so the test can eventually become false.
The book’s CalculateInterestTable program prints the principal for each year:
int year = 1;
while (year <= duration)
{
decimal interestPaid = principal * (interest / 100);
principal = decimal.Round(principal + interestPaid, 2);
Console.WriteLine($"{year}-{principal:F2}");
year++;
}
yearmust be declared and initialized before the loop.decimal.Round(value, 2)rounds to the nearest cent.- The new principal becomes the basis for the next year.
year++(the book writesyear = year + 1) guarantees progress toward the exit.
Sample: 10000 at 5.5% for 5 years prints 10550.00, 11130.25, 11742.41, 12388.24, 13069.59. After the fifth pass year is 6, the test fails, and control moves past the closing brace.
Warnings:
- The condition is not checked continuously. A loop does not stop the instant its condition becomes false; the test runs only when control returns to the top.
- Forgetting the update creates an infinite loop:
int nYear = 1; while (nYear < 10) { … }never ends. Stop a runaway console program with Ctrl+C.
Quick check: what value does year have when the loop ends?
8. The do…while loop
A do…while loop tests its condition at the end, so the body always runs at least once:
int year = 1;
do
{
// … some calculation …
year++;
} while (year < duration);
while |
do…while |
|
|---|---|---|
| When it tests | Before the body | After the body |
| Minimum passes | Zero | One |
| Good fit | Work that may not be needed at all | A prompt or menu that must appear once |
Note the semicolon after the closing while (…).
Quick check: which loop fits a menu that must display at least once?
9. break, continue, and forgiving input loops
breakpasses control to the first statement after the loop (or switch).continueskips the rest of this iteration and returns to the condition at the top.
Leaving a savings loop early once a goal is reached:
while (year <= duration)
{
principal += principal * (interest / 100);
Console.WriteLine($"{year}-{principal:F2}");
year++;
if (principal > maxPower * originalPrincipal)
{
break;
}
}
The book’s CalculateInterestTableMoreForgiving program replaces “quit on bad input” with “keep asking until the input is valid”:
decimal principal;
while (true)
{
Console.Write("Enter principal: ");
principal = Convert.ToDecimal(Console.ReadLine());
if (principal >= 0)
{
break;
}
Console.WriteLine("Principal cannot be negative");
Console.WriteLine("Try again");
Console.WriteLine();
}
Talking points:
while (true)is an intentional infinite loop; the embeddedbreakis the only exit, so make sure it can happen.- The condition is reversed. The error test
interest < 0 || interest > MaximumInterestbecomes the exit testinterest >= 0 && interest <= MaximumInterest. Reversing a condition flips each comparison and swaps||for&&. principalis declared before the loop so it still exists after the loop ends.- Explain exactly what the user did wrong before asking again, and show an example format when helpful.
- This version still crashes on blank or non-numeric input such as “seven”.
TryParsefrom Lesson 3 or the exception handling in Lesson 9 closes that gap.
Sample run: -1000 is rejected, 1000 accepted; -10 is rejected, 10 accepted; 5 years prints 1100.00 through 1610.51.
10. Scope rules
A variable declared inside braces exists only within those braces:
int days = 1;
while (days < duration)
{
int average = value / days;
// … use average …
days++;
}
// average does not exist here.
The declaration of average runs on every pass; C# treats it as a new variable each time and discards it at the closing brace. Experienced programmers say the scope of average is limited to the loop. If a value is needed after the loop, declare it before the loop, as the forgiving-input example does with principal.
Quick check: where must a value be declared if it is needed after the loop?
11. The for loop
The for loop gathers the three bookkeeping parts of a loop into its header:
for (initExpression; condition; incrementExpression)
{
// … body …
}
Order of execution:
initExpressionruns once.conditionis tested before every pass.- When true, the body runs.
- At the closing brace,
incrementExpressionruns, then control returns to the condition.
It is equivalent to this while loop:
initExpression;
while (condition)
{
// … body …
incrementExpression;
}
for (int year = 1; year <= duration; year++)
{
principal += principal * interest / 100;
Console.WriteLine($"{year}: {principal:F2}");
}
Talking points:
foradds no capability awhilelacks, but it makes the setup, exit criteria, and increment visible in one line. That makes it easier to read and harder to forget the update. It is the loop students will see most often in other people’s code.- A variable declared in the header, such as
year, is scoped to the loop; the scope includes the header and the body. - The postincrement
year++is customary in the header;++yearhas the same effect here. - C# does not force the first part to initialize and the last to increment, but anything else is poor practice.
- An empty condition is treated as true, so
for (;;)is an infinite loop.
Quick check: which part of the header runs only once? Rewrite a for loop as a while loop and confirm the output matches.
12. Nested loops
A loop contained within another loop is a nested loop. The inner loop runs to completion during each pass of the outer loop:
for (int row = 1; row <= 3; row++)
{
for (int col = 1; col <= 4; col++)
{
Console.Write($"({row},{col}) ");
}
Console.WriteLine();
}
- The outer loop advances the larger unit (each row); the inner loop completes every smaller unit (each column) for that row.
- Total body executions multiply: 3 rows × 4 columns = 12.
- The inner loop’s variable (
col) does not exist outside the inner loop. - Nested loops cannot cross: an inner loop must close before the outer loop closes.
- A
breakin the inner loop exits only the inner loop; the outer loop keeps running. C# has no single break that leaves both loops. When logic needs to escape several levels, place it in a method and usereturn(Lesson 12).
Quick check: how many body executions occur for 3 rows and 4 columns? Where does control go after an inner break?
Classroom application
Build a validated grade-entry program in stages:
- Read a score with a forgiving
while (true)loop that accepts only 0 through 100 and explains the rule on bad input. Name the limits withconstvalues. - Convert the score to a letter grade with
if/elsechains, then rewrite the conversion as a switch expression with relational patterns. - Wrap the program in a
do…whileloop that asks “Enter another? (y/n)” so the prompt always appears at least once. - Add a
forloop that prints a simple bar of*characters, one per 10 points.
Diagnose these before revealing answers:
- A
whileloop never stops because its counter stays 1. (Missing update.) - Two opposite conditions are written as separate
ifstatements. (Useelse.) - A menu must display once even if the repeat condition starts false. (Use
do…while.) - A status value matches no case. (Add
defaultor a discard arm.)
Common misconceptions
- A loop does not exit the moment its condition becomes false; the condition is checked only at the top of each pass (or bottom, for
do…while). - A
whileorforbody can run zero times; onlydo…whileguarantees one pass. - Forgetting to update the counting variable produces an infinite loop.
- Omitting braces on an
ifworks for one statement but invites bugs when lines are added. - Two separate
ifstatements are not the same asif/else. - A
switchcase label must be a constant; a variable is not allowed. - Case sections cannot fall through; each must end with
breakor another exit, though stacked labels can share a body. breakinside nested loops exits only the innermost loop.- A variable declared inside a loop or a
forheader does not exist after the loop. - The book’s
LangVersionproject edits are unnecessary with current .NET.
Lesson summary
Flow control turns a sequence of statements into a program that responds to its input. Branches (if, else, switch, switch expressions) state alternatives; loops (while, do…while, for) repeat work and need initialization, a test, and an update that can make the test false; and jumps (break, continue) change the trace locally. Scope rules tie every variable to the braces where it is declared. Lesson 6 extends repetition to collections with arrays, lists, and the foreach loop.
Course Notes