C# Control Flow: if/else, switch, Loops & Jump Statements
Complete Guide — 2025 Edition · Includes C# 8/9/10 switch expressions & pattern matching
⚡ Quick Answer: Control Flow in C#
- if/else/else-if — execute code based on boolean conditions
- switch statement — match an expression against case labels (strings, enums, types)
- switch expression (C# 8+) — concise value-producing switch with
=>arms - Pattern matching (C# 8/9+) — type, property, relational, and logical patterns in switch
- for / while / do-while / foreach — four loop types for every iteration need
- break / continue / goto — exit loops, skip iterations, jump to labels
Control flow is the foundation of any program — it determines which code runs, when, and how many times. This guide covers every C# control flow construct, including modern features like switch expressions and pattern matching that beginners often miss.
if / else
Conditions & branching
switch
Statement & expression
Pattern Matching
C# 8/9/10 features
Loops
for / while / foreach
Jump Statements
break / continue / goto
Modern C# Flow
??, ?., ternary, guards
Best Practices
Common mistakes & fixes
FAQ
Interview questions
if / else / else if — Conditions and Branching
The if statement evaluates a bool expression and executes its body
only when it is true. Unlike C/C++, C# requires a strict bool
— you cannot write if (x = 5) or if (1); the compiler rejects
non-boolean expressions.
using System;
int score = 78;
// Standard if / else if / else chain
if (score >= 90)
{
Console.WriteLine("Grade: A");
}
else if (score >= 70)
{
Console.WriteLine("Grade: B"); // ← this runs for score=78
}
else if (score >= 50)
{
Console.WriteLine("Grade: C");
}
else
{
Console.WriteLine("Grade: F");
}
// Nested if — find quadrant
int x = 3, y = -2;
if (x > 0)
{
if (y > 0) Console.WriteLine("Quadrant I");
else Console.WriteLine("Quadrant IV"); // ← x>0, y<0
}
else
{
if (y > 0) Console.WriteLine("Quadrant II");
else Console.WriteLine("Quadrant III");
}
Grade: B
Quadrant IVif (x = 5) is a
compile error in C# because the assignment returns int, not bool.
This eliminates the classic C/C++ bug where == is accidentally written as =.
switch — Classic Statement & Modern Expression
C# switch has evolved significantly. The original statement syntax is still valid but the modern switch expression (C# 8.0+) is shorter, more expressive, and type-checked for exhaustiveness.
break. Fall-through must use goto case.case int i when i > 0:. First major pattern matching addition.x switch { pat => val, _ => default }. Produces a value, no break needed, _ is default arm.>= 90, > 0 and < 100, not null.{ Address.City: "Seattle" }. Nested pattern matching.string day = "Wednesday";
switch (day)
{
case "Monday":
Console.WriteLine("Start of work week");
break;
case "Friday":
Console.WriteLine("End of work week");
break;
case "Saturday":
case "Sunday": // multiple cases share one block (empty fall-through OK)
Console.WriteLine("Weekend!");
break;
default:
Console.WriteLine("Midweek"); // ← runs for Wednesday
break;
}
// Explicit fall-through with goto case (the ONLY legal way in C#)
int errorCode = 404;
switch (errorCode)
{
case 404:
Console.WriteLine("Not Found");
goto case 0; // explicit fall-through — clearly intentional
case 0:
Console.WriteLine("(logging error)");
break;
}
Midweek
Not Found
(logging error)// Switch expression: produces a VALUE, no break needed
string day = "Monday";
string message = day switch
{
"Monday" => "Start of work week",
"Friday" => "End of work week",
"Saturday" or "Sunday" => "Weekend!", // C# 9 'or' pattern
_ => "Midweek" // _ = default arm
};
Console.WriteLine(message); // Start of work week
// Relational patterns (C# 9) — score to grade
int score = 82;
string grade = score switch
{
>= 90 => "A",
>= 70 and < 90 => "B", // ← matches 82
>= 50 and < 70 => "C",
< 0 => "Invalid",
_ => "F"
};
Console.WriteLine($"Score {score} → Grade {grade}"); // Grade B
// Property pattern — object property matching
var point = (3, -2);
string quadrant = point switch
{
( > 0, > 0) => "Quadrant I",
( < 0, > 0) => "Quadrant II",
( < 0, < 0) => "Quadrant III",
( > 0, < 0) => "Quadrant IV", // ← x=3, y=-2
_ => "On axis"
};
Console.WriteLine(quadrant);
Start of work week
Score 82 → Grade B
Quadrant IVPattern Matching in C# 8/9/10
Pattern matching lets you test an expression against a shape or value and extract information
in a single step. It's available in switch expressions, if statements
with is, and other contexts.
using System;
// 1. Type pattern with 'is' — declare and check in one step
object obj = "Hello";
if (obj is string s)
{
Console.WriteLine($"String of length {s.Length}"); // s is bound here
}
// 2. Null check pattern
string? name = null;
if (name is not null) // C# 9 'not' pattern — cleaner than != null
Console.WriteLine(name);
else
Console.WriteLine("name is null");
// 3. Type pattern in switch — heterogeneous collection
object[] items = { 42, "hello", 3.14, true, null };
foreach (var item in items)
{
string desc = item switch
{
int i when i > 0 => $"Positive int: {i}",
int i => $"Non-positive int: {i}",
string str => $"String: {str}",
double d => $"Double: {d}",
bool b => $"Bool: {b}",
null => "null value",
_ => "unknown"
};
Console.WriteLine(desc);
}
String of length 5
name is null
Positive int: 42
String: hello
Double: 3.14
Bool: True
null valueAll 4 Loop Types — for, while, do-while, foreach
| Loop | Condition checked | Min iterations | Best used when |
|---|---|---|---|
| for | Before each iteration | 0 | You know the exact count |
| while | Before each iteration | 0 | Count unknown, may never run |
| do-while | After each iteration | 1 (always runs once) | Body must run at least once (e.g., menu) |
| foreach | After each element | 0 | Iterating any collection (preferred) |
using System;
using System.Collections.Generic;
// 1. for — count-controlled
Console.WriteLine("for:");
for (int i = 1; i <= 3; i++)
Console.Write($"{i} ");
Console.WriteLine();
// 2. while — condition-first
Console.WriteLine("while:");
int j = 1;
while (j <= 3)
{
Console.Write($"{j} ");
j++;
}
Console.WriteLine();
// 3. do-while — body first, condition after
Console.WriteLine("do-while:");
int k = 1;
do
{
Console.Write($"{k} ");
k++;
} while (k <= 3);
Console.WriteLine();
// 4. foreach — collection iteration (preferred)
Console.WriteLine("foreach:");
var colors = new[] { "Red", "Green", "Blue" };
foreach (string color in colors)
Console.Write($"{color} ");
Console.WriteLine();
// foreach over Dictionary (key-value pairs)
Console.WriteLine("foreach dict:");
var scores = new Dictionary<string, int> { ["Alice"]=95, ["Bob"]=82 };
foreach (var (name, score) in scores)
Console.WriteLine($" {name}: {score}");
for: 1 2 3
while: 1 2 3
do-while: 1 2 3
foreach: Red Green Blue
foreach dict:
Alice: 95
Bob: 82IEnumerable<T>, and communicates intent
clearly. Use for when you need the index for logic, backwards iteration, or
modifying elements in place.
Jump Statements: break, continue, goto, return
using System;
// break — exit innermost loop immediately
Console.WriteLine("break:");
for (int i = 1; i <= 6; i++)
{
if (i == 4) break;
Console.Write($"{i} ");
}
Console.WriteLine(); // 1 2 3
// continue — skip to next iteration
Console.WriteLine("continue (odds only):");
for (int i = 1; i <= 6; i++)
{
if (i % 2 == 0) continue;
Console.Write($"{i} ");
}
Console.WriteLine(); // 1 3 5
// return inside foreach — cleanest way to exit nested loops
static bool ContainsNegative(int[] arr)
{
foreach (int x in arr)
if (x < 0) return true; // exits method + loop
return false;
}
Console.WriteLine(ContainsNegative(new[] { 1, -3, 5 })); // True
// goto case — only accepted goto use in switch
int code = 2;
switch (code)
{
case 1: Console.WriteLine("case 1"); break;
case 2:
Console.WriteLine("case 2 → also running case 3");
goto case 3; // explicit fall-through
case 3: Console.WriteLine("case 3"); break;
}
break: 1 2 3
continue (odds only): 1 3 5
True
case 2 → also running case 3
case 3return.
This is cleaner than a bool done flag or goto and makes the
intent obvious.
Modern C# Control Flow: ??, ?., Ternary & Guard Clauses
using System;
// Ternary operator — concise two-branch assignment
int score = 75;
string result = score >= 60 ? "Pass" : "Fail";
Console.WriteLine(result); // Pass
// ?? (null coalescing) — return left if non-null, else right
string? input = null;
string name = input ?? "Anonymous";
Console.WriteLine(name); // Anonymous
// ??= (null coalescing assignment, C# 8) — assign only if null
input ??= "Default";
Console.WriteLine(input); // Default
// ?. (null-conditional) — short-circuit on null
string? maybeNull = null;
int? len = maybeNull?.Length; // null, not NullReferenceException
Console.WriteLine(len ?? -1); // -1
// Guard clauses — early return to flatten nesting
static string ProcessInput(string? s)
{
if (s is null) return "Error: null input"; // guard
if (s.Length == 0) return "Error: empty string"; // guard
if (s.Length > 100) return "Error: too long"; // guard
// Happy path — unnested, clearly visible
return s.Trim().ToUpper();
}
Console.WriteLine(ProcessInput(null)); // Error: null input
Console.WriteLine(ProcessInput("hello")); // HELLO
Pass
Anonymous
Default
-1
Error: null input
HELLOC# vs C/C++ Control Flow Differences
| Feature | C# | C / C++ |
|---|---|---|
| if condition type | ✓ Must be bool | ✗ Any numeric (0=false) |
| if (x = 5) | ✓ Compile error | ✗ Silent bug — always true |
| switch on strings | ✓ Yes | ✗ No (C has no string switch) |
| switch fall-through | ✓ Compile error unless goto case | ✗ Silent implicit fall-through |
| switch expressions | ✓ C# 8+ (concise, value-producing) | ✗ Not available |
| Pattern matching | ✓ C# 7+ (type, property, relational) | ⚠ C++17 limited via if constexpr |
| foreach loop | ✓ Built-in, works on IEnumerable | ⚠ C++11 range-for (begin/end) |
| goto restrictions | ✓ Same method only + goto case | ✗ Any label in function, error-prone |
| Null coalescing ?? | ✓ Built-in operator | ✗ Not available |
| Null-conditional ?. | ✓ Built-in operator | ✗ Not available |
Best Practices & Common Mistakes
✅ Do these
- Use switch expressions (C# 8+) for value-to-value mappings — shorter and exhaustiveness-checked.
- Always include default /
_arm in every switch. - Prefer foreach over index-based for loops when iterating collections.
- Use guard clauses (early return) to reduce nesting — keep the happy path at low indentation.
- Use return inside a loop instead of complex break/flag patterns to exit early.
- Use ?? and ?. for null handling instead of verbose if-null checks.
❌ Avoid these
- Modifying a collection inside foreach — throws
InvalidOperationExceptionat runtime. Use aforloop or collect changes to apply after. - Infinite loops without guaranteed exit — always ensure your loop condition eventually becomes false.
- Off-by-one in for loops — double-check
<vs<=, and whether you start at 0 or 1. - Deep if/else nesting (more than 3 levels) — refactor with early returns, extract methods.
- goto for general flow — only acceptable as
goto casein a switch statement. - Chaining ternary operators —
a ? b : c ? d : eis unreadable. Use if/else or a switch expression.
FAQ / Interview Questions
case labels, requires break, and cannot produce a value directly. Switch expression (C# 8+) uses => arms, produces a value that can be assigned, requires no break, and uses _ for the default arm. For simple value mappings, switch expressions are preferred — they're shorter, cleaner, and the compiler warns when cases aren't exhaustive.break, return, throw, or goto case. To explicitly fall through to another case, use goto case X; — this makes the intent clear, unlike the silent fall-through bug in C/C++. Multiple empty cases sharing one body are allowed: case "Mon": case "Tue": ... break;while checks the condition before the first iteration — the body may never execute. do-while checks the condition after the first iteration — the body always executes at least once. Classic do-while use case: input prompts — you must show the prompt at least once regardless of initial state.foreach uses an enumerator (IEnumerator<T>) that tracks position. Adding or removing elements changes the collection's version number, which the enumerator detects and throws InvalidOperationException to prevent undefined behavior. Fix: use a regular for loop with an index (for arrays), or collect the items to add/remove in a separate list and apply the changes after the loop.if (input is null) return "Error"; if (input.Length == 0) return "Empty"; // main logic herecase int i), property value (case { Length: > 5 }), relational condition (case >= 90), or logical combination (case > 0 and < 100). Use it when: branching on the runtime type of an object (polymorphism without virtual methods), testing ranges of values, or matching complex conditions in a readable single expression. It replaces long if/else-if chains with type checks.Learn C# from zero to professional
Structured lessons, 200+ exercises, and a completion certificate. Join 50,000+ students.