Control Flow in C#
Master if/else, switch expressions, for/foreach/while loops, and pattern matching in C#.
if / else
if/else is the most fundamental control flow tool — it lets your program take different paths based on a condition. The conditions are boolean expressions, and C# evaluates them top to bottom, taking the first branch whose condition is true.
int temperature = 22;
if (temperature > 30)
{
Console.WriteLine("Hot");
}
else if (temperature > 20)
{
Console.WriteLine("Warm"); // This branch runs — first true condition wins
}
else if (temperature > 10)
{
Console.WriteLine("Cool");
}
else
{
Console.WriteLine("Cold");
}
Single-line bodies can omit braces, but adding them is generally safer and prevents bugs when adding more lines later:
// Acceptable for very simple cases like guard clauses
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
// Ternary — compact conditional assignment on one line
string label = score >= 50 ? "Pass" : "Fail";
switch Statement
The classic switch tests a single value against a fixed set of constants. It is cleaner than a long chain of if/else if when you have many specific values to handle. Multiple case labels can share a body by falling through to the same break.
string day = "Monday";
switch (day)
{
case "Monday":
case "Tuesday":
case "Wednesday":
case "Thursday":
case "Friday":
Console.WriteLine("Weekday"); // all five cases share this body
break;
case "Saturday":
case "Sunday":
Console.WriteLine("Weekend");
break;
default:
Console.WriteLine("Unknown day");
break;
}
switch Expression (C# 8+)
Switch expressions are more concise than switch statements and return a value directly. The compiler performs exhaustiveness checking — if there is a possible input that no arm handles, you get a compile-time warning. This makes switch expressions safer than chains of ternaries.
string DayType(string day) => day switch
{
"Saturday" or "Sunday" => "Weekend",
"Monday" or "Tuesday" or "Wednesday"
or "Thursday" or "Friday" => "Weekday",
_ => throw new ArgumentOutOfRangeException(nameof(day), day, null)
};
Console.WriteLine(DayType("Saturday")); // Weekend
Switch expressions really shine when combined with pattern matching, letting you branch on ranges, types, and property values all in one place:
// Range patterns — clean alternative to nested if/else if chains
string GetGrade(int score) => score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};
// Type patterns — dispatch logic based on the runtime type of an object
static string Describe(object obj) => obj switch
{
int n when n < 0 => $"Negative int: {n}",
int n => $"Positive int: {n}",
string s => $"String of length {s.Length}",
null => "null",
_ => $"Unknown: {obj.GetType().Name}"
};
// Property patterns — match on multiple properties at once
record Order(decimal Total, bool IsPriority);
string ShippingLabel(Order order) => order switch
{
{ IsPriority: true, Total: >= 100 } => "Free Priority",
{ IsPriority: true } => "Priority",
{ Total: >= 50 } => "Free Standard",
_ => "Standard ($5)"
};
for Loop
Use for when you need an explicit index or a precise number of iterations. The three parts of the for header — initializer, condition, iterator — give you full control over how the loop counter behaves.
// Classic for loop — index goes 0 to 9
for (int i = 0; i < 10; i++)
Console.Write(i + " "); // 0 1 2 3 4 5 6 7 8 9
// Reverse — count down from 9 to 0
for (int i = 9; i >= 0; i--)
Console.Write(i + " "); // 9 8 7 6 5 4 3 2 1 0
// Multiple variables in one for loop
for (int i = 0, j = 10; i < j; i++, j--)
Console.Write($"({i},{j}) ");
// Loop over array by index — useful when you need the position
string[] names = { "Alice", "Bob", "Carol" };
for (int i = 0; i < names.Length; i++)
Console.WriteLine($"{i}: {names[i]}");
foreach Loop
foreach is the idiomatic way to iterate a collection when you do not need the index. It is cleaner than a for loop and works with any type that implements IEnumerable<T> — not just arrays and lists.
var fruits = new List<string> { "apple", "banana", "cherry" };
// Simple iteration — no index management needed
foreach (string fruit in fruits)
Console.WriteLine(fruit);
// With index using LINQ's Select overload — when you need both value and position
foreach (var (fruit, index) in fruits.Select((f, i) => (f, i)))
Console.WriteLine($"{index}: {fruit}");
// Iterating a dictionary — deconstruct each key-value pair
var scores = new Dictionary<string, int>
{
["Alice"] = 95,
["Bob"] = 82
};
foreach (var (name, score) in scores)
Console.WriteLine($"{name}: {score}");
// Iterate a string — yields each character in order
foreach (char c in "Hello")
Console.Write(c + " "); // H e l l o
while and do-while
while runs as long as a condition is true, checking it before each iteration. do-while always executes the body at least once before checking. Use do-while for input loops and retry patterns where you always need at least one attempt.
// while — checks condition before each iteration
int count = 0;
while (count < 5)
{
Console.Write(count + " ");
count++;
}
// 0 1 2 3 4
// do-while — body runs at least once, condition checked afterwards
// Perfect for "keep asking until valid input" patterns
string input;
do
{
Console.Write("Enter 'quit' to exit: ");
input = Console.ReadLine() ?? "";
} while (input != "quit");
break and continue
break and continue give you fine-grained control over loop execution without restructuring the entire loop. break exits immediately; continue skips the rest of the current iteration and moves on to the next check.
// break — exit the loop immediately when condition is met
for (int i = 0; i < 100; i++)
{
if (i == 5)
break;
Console.Write(i + " "); // 0 1 2 3 4
}
// continue — skip even numbers, process only odd ones
for (int i = 0; i < 10; i++)
{
if (i % 2 == 0)
continue; // jump to next iteration
Console.Write(i + " "); // 1 3 5 7 9
}
// break in nested loops — only breaks the innermost loop
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (j == 1) break; // exits inner loop only
Console.Write($"({i},{j}) ");
}
}
// (0,0) (1,0) (2,0)
To break out of an outer loop, either use a flag or restructure into a method. The method approach is generally cleaner:
// Using a flag — works but adds noise
bool found = false;
for (int i = 0; i < rows && !found; i++)
for (int j = 0; j < cols && !found; j++)
if (grid[i][j] == target)
found = true;
// Better: extract to a method — return exits all loops at once
bool ContainsTarget(int[][] grid, int target)
{
for (int i = 0; i < grid.Length; i++)
for (int j = 0; j < grid[i].Length; j++)
if (grid[i][j] == target) return true;
return false;
}
Pattern Matching in Control Flow
Pattern matching lets if and switch do more than just compare values. You can simultaneously check the type, bind a variable, and test properties — all in one readable expression.
// is pattern with variable binding — type check and cast in one step
object result = GetResult();
if (result is string message)
Console.WriteLine($"Success: {message}");
else if (result is Exception ex)
Console.WriteLine($"Error: {ex.Message}");
else if (result is int code and > 0)
Console.WriteLine($"Code: {code}");
// Tuple patterns — match on multiple values simultaneously
var point = (x: 3, y: -1);
string quadrant = point switch
{
(> 0, > 0) => "Q1",
(< 0, > 0) => "Q2",
(< 0, < 0) => "Q3",
(> 0, < 0) => "Q4",
_ => "On axis"
};
Console.WriteLine(quadrant); // Q4
Exception-Based Control Flow
Exceptions exist for exceptional situations — unexpected failures, not expected outcomes. Using exceptions for ordinary control flow (like checking if a string is a valid number) is expensive and obscures intent. Prefer Try* methods that return bool for expected failure cases.
// Avoid using exceptions for normal control flow
// Bad — exceptions are expensive and not meant for this
bool ParseIntBad(string s, out int result)
{
try
{
result = int.Parse(s);
return true;
}
catch
{
result = 0;
return false;
}
}
// Good — TryParse is specifically designed for "try this, it might fail"
bool ParseIntGood(string s, out int result)
=> int.TryParse(s, out result);
Practical Example: FizzBuzz with Switch
This example shows how tuple patterns and switch expressions combine into readable, compact logic — no nested ifs required.
for (int i = 1; i <= 20; i++)
{
// Tuple pattern: test both (i % 3) and (i % 5) simultaneously
string output = (i % 3, i % 5) switch
{
(0, 0) => "FizzBuzz", // divisible by both
(0, _) => "Fizz", // divisible by 3 only
(_, 0) => "Buzz", // divisible by 5 only
_ => i.ToString() // neither
};
Console.WriteLine(output);
}