Free Handbook · Every example compiled & verified

Flow Control

if and else, the ternary operator, switch statements, switch expressions with pattern matching, and the four C# loops with break and continue.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 02 · what you'll be able to do

  • Branch with if, else if and else, and write a short choice as a ternary expression
  • Use a switch statement with multiple case labels, when guards and a default, without fall-through bugs
  • Replace long if chains with a switch expression using relational, and, or and not patterns
  • Pick between while, do-while, for and foreach, and control them with break and continue
  • Trace a loop line by line and predict its output before running it
01

if, else if, else and the ternary operator

An if statement runs a block only when its condition is true. The condition must be a bool — C# will not treat 0, null or an empty string as false the way C or JavaScript do. Chain alternatives with else if; the first true condition wins and the rest are skipped. A final else catches everything left.

C#Program.cs
int[] scores = { 95, 82, 67, 40 };

foreach (int score in scores)
{
    string grade;
    if (score >= 90)
        grade = "A";
    else if (score >= 75)
        grade = "B";
    else if (score >= 50)
        grade = "C";
    else
        grade = "F";

    Console.WriteLine($"{score} -> {grade}");
}

int age = 20;
bool hasTicket = true;
if (age >= 18 && hasTicket)
{
    Console.WriteLine("You may enter.");
}
Outputcompiled & run with real C#
95 -> A
82 -> B
67 -> C
40 -> F
You may enter.
Your turn

Add an A+ grade for 98 and above. Where does the new branch have to go, and why?

Always use braces
Without braces, if controls only the next single statement. Add a second indented line later and it runs every time, whatever the condition — the indentation lies. Most C# style guides (and dotnet format rules) require braces on every branch for that reason; the short form above is fine only for one-line branches.

The ternary operator condition ? a : b is an expression: it produces a value, so you can assign it, return it or put it inside an interpolated string. Use it for a simple choice between two values, and fall back to if when either side needs more than one step.

C#Program.cs
int items = 1;
Console.WriteLine($"{items} item{(items == 1 ? "" : "s")}");

items = 3;
Console.WriteLine($"{items} item{(items == 1 ? "" : "s")}");

decimal total = 1200m;
decimal shipping = total >= 999m ? 0m : 49m;
Console.WriteLine($"Shipping: {shipping}");

int temp = 31;
string feel = temp > 30 ? "hot" : temp > 15 ? "mild" : "cold";
Console.WriteLine(feel);
Outputcompiled & run with real C#
1 item
3 items
Shipping: 0
hot

Inside an interpolated string, a ternary needs parentheses, because : would otherwise start a format specifier.

Error you will hit

CS0029: = instead of == in a condition

C#
int x = 5;
if (x = 5)
    Console.WriteLine("five");
Program.cs(2,5): error CS0029: Cannot implicitly convert type 'int' to 'bool'
Why the compiler said that

= is assignment, and x = 5 is an expression whose value is the int 5. An if needs a bool, and C# never treats a number as true or false. In C this bug compiles and always runs the branch; in C# it cannot.

The fix

Use == to compare.

C#
int x = 5;
if (x == 5)
    Console.WriteLine("five");
02

The switch statement

A switch statement compares one value against a list of case labels and runs the matching section. Several labels can share one section. Every section must end with break (or return, throw, continue) — C# does not let execution fall through from one section into the next. default handles anything not listed, and a when clause adds an extra condition to a case.

C#Program.cs
string[] commands = { "start", "STOP", "pause", "help", "jump" };

foreach (string cmd in commands)
{
    switch (cmd.ToLowerInvariant())
    {
        case "start":
            Console.WriteLine("Starting...");
            break;
        case "stop":
        case "pause":               // two labels, one section
            Console.WriteLine($"Halting ({cmd})");
            break;
        case var c when c.StartsWith("h"):
            Console.WriteLine("Commands: start, stop, pause");
            break;
        default:
            Console.WriteLine($"Unknown command: {cmd}");
            break;
    }
}
Outputcompiled & run with real C#
Starting...
Halting (STOP)
Halting (pause)
Commands: start, stop, pause
Unknown command: jump
Your turn

Add a "restart" case that prints two lines. Then remove its break and read the compiler error.

Error you will hit

CS0163: a case section falls through

C#
int level = 1;
switch (level)
{
    case 1:
        Console.WriteLine("low");
    case 2:
        Console.WriteLine("medium");
        break;
}
Program.cs(4,5): error CS0163: Control cannot fall through from one case label ('case 1:') to another
Why the compiler said that

In C and Java a missing break silently runs the next case too — a classic bug. C# turns it into a compile error. Empty labels stacked together (case "stop": case "pause":) are still allowed, because there is no code in between.

The fix

End the section with break. If you really want to continue into another case, say so with goto case 2;.

C#
int level = 1;
switch (level)
{
    case 1:
        Console.WriteLine("low");
        break;
    case 2:
        Console.WriteLine("medium");
        break;
}
03

Switch expressions and pattern matching

A switch expression (C# 8+) is the compact, value-producing form: value switch { pattern => result, ... }. Each arm is a pattern and a result; the first matching arm wins; _ is the discard pattern that matches anything. Patterns are much richer than constants: relational patterns (< 0, >= 90), combined with and, or and not, plus type and property patterns.

C#Program.cs
foreach (int t in new[] { -5, 0, 12, 24, 38 })
    Console.WriteLine($"{t,3}C: {Describe(t)}");

foreach (char c in "a7 Z")
    Console.WriteLine($"'{c}' is {Kind(c)}");

static string Describe(int celsius) => celsius switch
{
    < 0 => "freezing",
    0 => "exactly zero",
    > 0 and < 15 => "cold",
    >= 15 and < 30 => "pleasant",
    _ => "hot",
};

static string Kind(char c) => c switch
{
    >= 'a' and <= 'z' or >= 'A' and <= 'Z' => "a letter",
    >= '0' and <= '9' => "a digit",
    not ' ' => "a symbol",
    _ => "a space",
};
Outputcompiled & run with real C#
 -5C: freezing
  0C: exactly zero
 12C: cold
 24C: pleasant
 38C: hot
'a' is a letter
'7' is a digit
' ' is a space
'Z' is a letter
Your turn

Add an arm for exactly 100 (100 => "boiling"). Does it have to go before or after the _ arm?

Patterns also look inside objects. A type pattern (int n) checks the type and gives you a typed variable; a property pattern ({ Length: 0 }) checks members; a tuple pattern switches on several values at once. The same patterns work after is in an if.

C#Program.cs
object[] inputs = { 42, -3, "", "hello", 2.5, null! };
foreach (object o in inputs)
    Console.WriteLine(Label(o));

Console.WriteLine(Move("rock", "scissors"));
Console.WriteLine(Move("paper", "paper"));

object box = 7;
if (box is int n and > 5)
    Console.WriteLine($"big int {n}");

static string Label(object? o) => o switch
{
    null => "null",
    int n when n < 0 => $"negative int {n}",
    int n => $"int {n}",
    string { Length: 0 } => "empty string",
    string s => $"string of {s.Length}",
    _ => $"something else: {o.GetType().Name}",
};

static string Move(string a, string b) => (a, b) switch
{
    var (x, y) when x == y => "draw",
    ("rock", "scissors") or ("scissors", "paper") or ("paper", "rock") => "first wins",
    _ => "second wins",
};
Outputcompiled & run with real C#
int 42
negative int -3
empty string
string of 5
something else: Double
null
first wins
draw
big int 7
Error you will hit

CS8509 then SwitchExpressionException: a switch expression with a gap

C#
int day = 7;
string kind = day switch
{
    >= 1 and <= 5 => "weekday",
    6 => "saturday",
};
Console.WriteLine(kind);
Program.cs(2,19): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '0' is not covered.
Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.
Unmatched value was 7.
   at <PrivateImplementationDetails>.ThrowSwitchExpressionException(Object unmatchedValue)
   at Program.<Main>$(String[] args) in Program.cs:line 2
Why the compiler said that

A switch expression must produce a value for every input. The compiler proved it does not (it even names a missing value, 0) but only warned, so the program built and ran. With 7 no arm matched, and there is no value to return, so it throws. Never ignore CS8509.

The fix

Cover every case — usually with a final _ arm that returns a fallback or throws a clear exception of your own.

C#
int day = 7;
string kind = day switch
{
    >= 1 and <= 5 => "weekday",
    6 => "saturday",
    7 => "sunday",
    _ => throw new ArgumentOutOfRangeException(nameof(day)),
};
Console.WriteLine(kind);
Error you will hit

CS8510: an arm that can never match

C#
int temp = 30;
string feel = temp switch
{
    > 10 => "mild",
    > 25 => "hot",
    _ => "cold",
};
Console.WriteLine(feel);
Program.cs(5,5): error CS8510: The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match.
Why the compiler said that

Arms are tested top to bottom. Anything greater than 25 is also greater than 10, so the first arm always catches it and "hot" could never be returned. The compiler spots the dead arm and refuses to build.

The fix

Put the most specific patterns first, or make the ranges not overlap.

C#
int temp = 30;
string feel = temp switch
{
    > 25 => "hot",
    > 10 => "mild",
    _ => "cold",
};
Console.WriteLine(feel);

switch statement

  • Runs statements; produces no value
  • Needs break at the end of each section
  • Good when each case does several things
  • Missing cases are silently skipped

switch expression

  • Produces a value you can assign or return
  • Arms separated by commas, no break
  • Good for mapping an input to a result
  • Missing cases warn at compile time and throw at runtime
04

while and do-while loops

A while loop checks its condition before each pass, so its body may run zero times. Use it when you do not know how many passes you need — keep going until something happens. A do-while loop checks after each pass, so its body always runs at least once: the natural shape for "ask, then check the answer".

C#Program.cs
// while: how many doublings until we pass 1000?
int value = 1;
int steps = 0;
while (value <= 1000)
{
    value *= 2;
    steps++;
}
Console.WriteLine($"{steps} doublings -> {value}");

// digits of a number, right to left
int n = 4096;
while (n > 0)
{
    Console.Write(n % 10 + " ");
    n /= 10;
}
Console.WriteLine();
Outputcompiled & run with real C#
10 doublings -> 1024
6 9 0 4
Your turn

Change the second loop to add the digits up instead of printing them. What is the digit sum of 4096?

C#Program.cs
int number;
do
{
    Console.Write("Enter a positive number: ");
    string? line = Console.ReadLine();
    if (!int.TryParse(line, out number) || number <= 0)
    {
        Console.WriteLine($"'{line}' is not valid.");
        number = 0;
    }
} while (number <= 0);

Console.WriteLine($"Thanks, you entered {number}.");
Outputcompiled & run with real C#
Enter a positive number: 'abc' is not valid.
Enter a positive number: '-4' is not valid.
Enter a positive number: Thanks, you entered 7.

The program was fed three lines: abc, -4 and 7. The prompt must appear before the first answer, so do-while fits.

Infinite loops
If nothing inside a while ever makes the condition false, the loop never ends. Before you run a loop, point at the line that changes the variable in the condition. while (true) is legitimate only with a break or return inside — for example, a server that loops until it is told to stop.
05

for loops and tracing a loop

A for loop puts the three parts of a counting loop on one line: for (initialiser; condition; iterator). The initialiser runs once, the condition is checked before every pass, and the iterator runs after every pass. A variable declared in the initialiser exists only inside the loop.

C#Program.cs
for (int i = 1; i <= 5; i++)
    Console.Write(i + " ");
Console.WriteLine();

for (int i = 10; i > 0; i -= 3)       // count down in threes
    Console.Write(i + " ");
Console.WriteLine();

// nested loops: a small multiplication table
for (int row = 1; row <= 3; row++)
{
    for (int col = 1; col <= 4; col++)
        Console.Write($"{row * col,4}");
    Console.WriteLine();
}
Outputcompiled & run with real C#
1 2 3 4 5 
10 7 4 1 
   1   2   3   4
   2   4   6   8
   3   6   9  12
Your turn

Print a right-angled triangle of * five rows high: one star on the first row, five on the last.

VisualizeSumming the odd numbers below 8Step 1 / 9
int sum = 0;
for (int i = 1; i < 8; i += 2)
{
sum += i;
}
Console.WriteLine(sum);
Line 1

sum starts at 0.

Variables now
sum0
All 9 steps as a table
StepLineWhat happenedVariables now
11sum starts at 0.sum = 0
22The initialiser runs once: i = 1. Is 1 < 8? Yes, enter the body.sum = 0 i = 1
34Add 1.sum = 1 i = 1
42Iterator: i += 2 makes 3. 3 < 8, so loop again.sum = 1 i = 3
54Add 3.sum = 4 i = 3
64Next pass, i is 5: add 5.sum = 9 i = 5
74Next pass, i is 7: add 7.sum = 16 i = 7
82Iterator makes i 9. 9 < 8 is false, so the loop ends and i goes out of scope.sum = 16
96Print the total.
Error you will hit

CS0103: using a loop variable outside the loop

C#
for (int i = 0; i < 3; i++)
{
    int doubled = i * 2;
}
Console.WriteLine(doubled);
Program.cs(5,19): error CS0103: The name 'doubled' does not exist in the current context
Why the compiler said that

A variable lives only inside the { } block it is declared in — its scope. doubled (and i) disappear when the loop ends. Each pass also gets a fresh doubled.

The fix

Declare the variable before the loop if you need its value afterwards.

C#
int doubled = 0;
for (int i = 0; i < 3; i++)
{
    doubled = i * 2;
}
Console.WriteLine(doubled);
06

foreach, break and continue

foreach visits every item of an array, a string, a list — anything that is IEnumerable — without an index to manage, so there is no off-by-one bug to write. Inside any loop, continue skips the rest of this pass and moves to the next one, and break leaves the loop entirely.

C#Program.cs
string[] files = { "a.txt", "b.tmp", "c.txt", "STOP", "d.txt" };

foreach (string f in files)
{
    if (f == "STOP") break;              // leave the loop
    if (!f.EndsWith(".txt")) continue;   // skip this one
    Console.WriteLine($"processing {f}");
}

int vowels = 0;
foreach (char c in "Programming in C#")
{
    if ("aeiouAEIOU".Contains(c)) vowels++;
}
Console.WriteLine($"vowels: {vowels}");
Outputcompiled & run with real C#
processing a.txt
processing c.txt
vowels: 4
Your turn

Find the first number in { 3, 8, 15, 22 } divisible by 5 and stop searching as soon as you find it.

break leaves only the innermost loop. C# has no labelled break, so to escape two loops at once, put them in a method and return — the cleanest option — or use a flag.

C#Program.cs
int[,] grid =
{
    { 4, 9, 2 },
    { 3, 5, 7 },
    { 8, 1, 6 },
};

Console.WriteLine(Find(grid, 7));
Console.WriteLine(Find(grid, 10));

static string Find(int[,] g, int target)
{
    for (int r = 0; r < g.GetLength(0); r++)
        for (int c = 0; c < g.GetLength(1); c++)
            if (g[r, c] == target)
                return $"found {target} at row {r}, col {c}";  // exits both loops
    return $"{target} not found";
}
Outputcompiled & run with real C#
found 7 at row 1, col 2
10 not found
Error you will hit

InvalidOperationException: changing a list inside its own foreach

C#
var names = new List<string> { "ana", "", "ben" };
foreach (string n in names)
{
    if (n == "") names.Remove(n);
}
Console.WriteLine(names.Count);
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.List`1.Enumerator.MoveNext()
   at Program.<Main>$(String[] args) in Program.cs:line 2
Why the compiler said that

foreach walks the list with an enumerator that remembers its position. Removing an item shifts everything after it, so the enumerator detects the change on its next step (MoveNext, reported on the foreach line) and throws rather than skip or repeat items.

The fix

Use RemoveAll, loop backwards with a for and an index, or build a new list.

C#
var names = new List<string> { "ana", "", "ben" };
names.RemoveAll(n => n == "");
Console.WriteLine(names.Count);
LoopCondition checkedUse when
whilebefore each pass (may run 0 times)you loop until something changes
do-whileafter each pass (runs at least once)you must act once before you can check, such as prompting
forbefore each pass, with a counteryou need the index or a fixed number of passes
foreachuntil the items run outyou visit every item of a collection (the default choice)
Condition
An expression of type bool that decides whether a branch or loop body runs.
Ternary operator
c ? a : b, an expression that evaluates to a when c is true and b otherwise.
Fall-through
Running from one switch case into the next; a compile error in C# (CS0163).
Switch expression
x switch { pattern => value, ... }, which maps an input to a value using patterns.
Pattern
A test applied to a value: constant, relational (> 5), type, property, tuple, combined with and/or/not.
Discard pattern
_, which matches anything; the last arm of most switch expressions.
Exhaustive
A switch expression that handles every possible input; otherwise CS8509 and a runtime exception.
Scope
The block in which a variable exists; a loop variable is gone once the loop ends.
break / continue
break leaves the innermost loop or switch; continue skips to the loop's next pass.
Quick check

How many times does the body of do { n++; } while (n < 0); run when n starts at 5?

Quick check

A switch expression has no arm for the value it receives and no _ arm. What happens?

Frequently asked questions

Does C# switch fall through like C or Java?
No. Every non-empty case section must end with break, return, throw or goto; otherwise the compiler reports CS0163. Stacking empty labels such as case 1: case 2: is allowed, and goto case lets you jump to another case on purpose.
What is the difference between a switch statement and a switch expression in C#?
A switch statement runs blocks of statements and produces no value. A switch expression, added in C# 8, maps an input to a value using patterns, needs no break, and warns at compile time when it does not cover every input.
How do I break out of nested loops in C#?
C# has no labelled break. Move the nested loops into a method and return when you find what you need, or set a flag that the outer loop checks. goto also works but is rarely used in modern code.

Finish the C# handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.