Beginner

Control Flow in C#

Learn how to direct program execution in C# with if/else conditionals, switch statements and expressions, loops, and loop control, with runnable Docker examples

Control flow is how a program decides what to do and how many times to do it. Without it, code would run top to bottom exactly once with no decisions and no repetition. C# gives you the full toolbox the C family is known for — if/else, switch, for, while, do/while — and then layers modern, expression-oriented constructs on top, most notably the switch expression and pattern matching that arrived with C# 8 and have grown in every release since.

As a multi-paradigm language, C# lets you choose your style. You can write imperative, statement-based control flow that looks almost identical to C or Java, or you can lean into the functional side with expressions that return values instead of mutating state. The switch keyword captures this duality perfectly: there is a classic switch statement that executes branches, and a switch expression that evaluates to a result. Both are idiomatic; which you reach for depends on whether you want a side effect or a value.

Because C# is statically and strongly typed, the compiler checks your conditions and patterns ahead of time. A switch over an enum can warn you about unhandled cases, an if condition must actually be a bool (no truthy integers as in C), and pattern matches are verified for type compatibility. In this tutorial we will work through conditionals, both flavors of switch, the loop family, and loop control with break and continue — each as a complete, runnable program.

Conditionals: if, else if, else

The if statement is the most fundamental branch. Its condition must evaluate to a bool — unlike C, you cannot use a bare integer as a truth value. You can chain conditions with else if, fall through to a final else, and combine boolean tests with the short-circuiting && and || operators. For simple either/or value selection, the ternary ?: operator gives you a compact conditional expression.

Create a file named Program.cs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int temperature = 72;

if (temperature < 32)
{
    Console.WriteLine("Freezing");
}
else if (temperature < 60)
{
    Console.WriteLine("Cold");
}
else if (temperature < 80)
{
    Console.WriteLine("Comfortable");
}
else
{
    Console.WriteLine("Hot");
}

// Combining conditions with logical operators
int hour = 14;
bool isWeekend = false;

if (hour >= 9 && hour < 17 && !isWeekend)
{
    Console.WriteLine("The office is open");
}
else
{
    Console.WriteLine("The office is closed");
}

// Ternary conditional expression: a compact if/else that produces a value
int score = 85;
string result = score >= 60 ? "Pass" : "Fail";
Console.WriteLine($"Score {score}: {result}");

Run it:

1
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:9.0 sh -c "cp Program.cs Program.cs.bak && dotnet new console -o . --force >/dev/null 2>&1 && cp Program.cs.bak Program.cs && rm Program.cs.bak && dotnet run"

Expected output:

Comfortable
The office is open
Score 85: Pass

The switch Statement

When you need to branch on many discrete values of a single expression, a switch statement is clearer than a long else if chain. Each case label is followed by statements and must end with break (C# does not allow accidental fall-through between non-empty cases). You can stack multiple labels to share a body, and default catches anything unmatched.

Create a file named Program.cs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int dayNumber = 3;
string dayName;

switch (dayNumber)
{
    case 1:
        dayName = "Monday";
        break;
    case 2:
        dayName = "Tuesday";
        break;
    case 3:
        dayName = "Wednesday";
        break;
    case 6:
    case 7:                 // stacked labels share one body
        dayName = "Weekend";
        break;
    default:
        dayName = "Unknown";
        break;
}

Console.WriteLine($"Day {dayNumber} is {dayName}");

Run it with the same Docker command shown above. Expected output:

Day 3 is Wednesday

The switch Expression and Pattern Matching

Modern C# (8.0+) adds the switch expression, which evaluates to a value rather than executing statements. It is more concise, requires every arm to produce a result, and supports rich patterns: relational patterns (< 10), the discard _ for the default arm, when guards, and tuple patterns that match several values at once. This is C#’s functional side showing through.

Create a file named Program.cs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Relational patterns: each arm produces a string
string Categorize(int n) => n switch
{
    < 0   => "negative",
    0     => "zero",
    < 10  => "small",
    < 100 => "medium",
    _     => "large"
};

Console.WriteLine($"7   -> {Categorize(7)}");
Console.WriteLine($"42  -> {Categorize(42)}");
Console.WriteLine($"500 -> {Categorize(500)}");

// Tuple patterns with a `when` guard match two values together
string RockPaperScissors(string a, string b) => (a, b) switch
{
    ("rock", "scissors")  => "Player A wins",
    ("scissors", "paper") => "Player A wins",
    ("paper", "rock")     => "Player A wins",
    _ when a == b         => "Tie",
    _                     => "Player B wins"
};

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

Run it with the same Docker command. Expected output:

7   -> small
42  -> medium
500 -> large
Player A wins
Tie
Player B wins

Loops: for, while, do-while, and foreach

C# offers four loop forms. The for loop is ideal when you know the iteration count and need an index. The while loop runs as long as a condition holds, checking before each pass. The do/while loop checks after each pass, so its body always runs at least once. And foreach iterates directly over the elements of any collection without an index — the most common and readable choice when you just need each item.

Create a file named Program.cs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// for loop: known iteration count with an index
Console.WriteLine("for loop (squares):");
for (int i = 1; i <= 5; i++)
{
    Console.WriteLine($"  {i} squared is {i * i}");
}

// while loop: condition checked before each pass
Console.WriteLine("while loop (countdown):");
int count = 3;
while (count > 0)
{
    Console.WriteLine($"  {count}...");
    count--;
}
Console.WriteLine("  Liftoff!");

// do-while loop: body always runs at least once
Console.WriteLine("do-while loop:");
int n = 10;
do
{
    Console.WriteLine($"  n = {n}");
    n += 5;
} while (n < 10);

// foreach loop: iterate over a collection's elements directly
Console.WriteLine("foreach loop:");
string[] languages = { "C#", "F#", "VB.NET" };
foreach (string lang in languages)
{
    Console.WriteLine($"  {lang}");
}

Run it with the same Docker command. Expected output:

for loop (squares):
  1 squared is 1
  2 squared is 4
  3 squared is 9
  4 squared is 16
  5 squared is 25
while loop (countdown):
  3...
  2...
  1...
  Liftoff!
do-while loop:
  n = 10
foreach loop:
  C#
  F#
  VB.NET

Notice the do/while loop printed n = 10 even though 10 < 10 is false — the body runs before the condition is checked.

Loop Control: break and continue

Inside any loop, continue skips the rest of the current iteration and jumps to the next one, while break exits the loop entirely. They let you express “skip this case” and “I’m done early” without contorting your loop condition.

Create a file named Program.cs:

1
2
3
4
5
6
7
8
Console.WriteLine("Odd numbers, stopping once we pass 7:");
for (int i = 1; i < 20; i++)
{
    if (i % 2 == 0) continue;   // skip even numbers
    if (i > 7) break;           // stop once we go beyond 7
    Console.WriteLine($"  {i}");
}
Console.WriteLine("Done.");

Run it with the same Docker command. Expected output:

Odd numbers, stopping once we pass 7:
  1
  3
  5
  7
Done.

Running with Docker

Each example in this tutorial is a complete program. Save it as Program.cs and run it with:

1
2
3
4
5
# Pull the official .NET SDK image (one-time)
docker pull mcr.microsoft.com/dotnet/sdk:9.0

# Compile and run Program.cs
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:9.0 sh -c "cp Program.cs Program.cs.bak && dotnet new console -o . --force >/dev/null 2>&1 && cp Program.cs.bak Program.cs && rm Program.cs.bak && dotnet run"

The command preserves your Program.cs, scaffolds a temporary project around it, compiles, and runs it — no local .NET SDK required.

Key Concepts

  • Conditions must be bool in C#. Unlike C, you cannot use a bare integer or null as a truth value — this catches a whole class of bugs at compile time.
  • The switch statement branches on discrete values and requires break to end each non-empty case; there is no accidental fall-through. Stack case labels to share a body.
  • The switch expression (C# 8+) returns a value and supports rich patterns — relational (< 10), tuple, and when guards — bringing a functional style to branching.
  • Arms are evaluated top to bottom, so order your patterns from most specific to most general and end with the discard _ to catch everything else.
  • Four loop forms cover every need: for (indexed, known count), while (check before), do/while (check after, runs at least once), and foreach (iterate a collection directly).
  • foreach is the idiomatic choice for walking a collection when you don’t need the index — it reads clearly and avoids off-by-one errors.
  • continue skips to the next iteration and break exits the loop entirely, letting you keep loop conditions simple.
  • The ternary ?: operator is a compact conditional expression for either/or value selection, complementing statement-based if/else.

Running Today

All examples can be run using Docker:

docker pull mcr.microsoft.com/dotnet/sdk:9.0
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining