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.

  • csharp
  • control-flow
  • if-else
  • switch
  • loops
  • 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 nested if statements that validate input.
  • Use a switch statement, including shared case labels and a default case, and state its restrictions.
  • Read and write a switch expression with relational and logical patterns and a discard (_) arm.
  • Trace while, do…while, and for loops, including the initialize, test, body, and update steps.
  • Use break and continue correctly, including inside nested loops.
  • Explain block scope for variables declared inside loops and for headers.
  • 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.Write keeps the cursor on the prompt line. Tell the user exactly what to enter; an uninformative prompt such as > confuses users.
  • ReadLine() returns text, so Convert.ToDecimal() turns it into a number. ReadLine(), WriteLine(), and ToDecimal() 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 50 inside 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 uses const because 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 switch is evaluated once.
  • Case labels: each case is compared to that value.
  • Exit: break (or another exit such as return) leaves the switch.
  • Fallback: default handles 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 a string; 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 when x is a constant.
  • Each case section must end with break or 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 switch is 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++;
}
  • year must 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 writes year = 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

  • break passes control to the first statement after the loop (or switch).
  • continue skips 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 embedded break is the only exit, so make sure it can happen.
  • The condition is reversed. The error test interest < 0 || interest > MaximumInterest becomes the exit test interest >= 0 && interest <= MaximumInterest. Reversing a condition flips each comparison and swaps || for &&.
  • principal is 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”. TryParse from 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:

  1. initExpression runs once.
  2. condition is tested before every pass.
  3. When true, the body runs.
  4. At the closing brace, incrementExpression runs, 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:

  • for adds no capability a while lacks, 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; ++year has 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 break in 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 use return (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:

  1. 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 with const values.
  2. Convert the score to a letter grade with if/else chains, then rewrite the conversion as a switch expression with relational patterns.
  3. Wrap the program in a do…while loop that asks “Enter another? (y/n)” so the prompt always appears at least once.
  4. Add a for loop that prints a simple bar of * characters, one per 10 points.

Diagnose these before revealing answers:

  • A while loop never stops because its counter stays 1. (Missing update.)
  • Two opposite conditions are written as separate if statements. (Use else.)
  • A menu must display once even if the repeat condition starts false. (Use do…while.)
  • A status value matches no case. (Add default or 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 while or for body can run zero times; only do…while guarantees one pass.
  • Forgetting to update the counting variable produces an infinite loop.
  • Omitting braces on an if works for one statement but invites bugs when lines are added.
  • Two separate if statements are not the same as if/else.
  • A switch case label must be a constant; a variable is not allowed.
  • Case sections cannot fall through; each must end with break or another exit, though stacked labels can share a body.
  • break inside nested loops exits only the innermost loop.
  • A variable declared inside a loop or a for header does not exist after the loop.
  • The book’s LangVersion project 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.