Control Flow Statements

Master C# control flow including conditionals, loops, pattern matching, and modern switch expressions

C#
Control Flow
Fundamentals
Pattern Matching
18 min read

Overview

Modern C# has evolved control flow with pattern matching, switch expressions, and enhanced foreach capabilities. Understanding these features leads to more readable and maintainable code.

Core Concepts

Conditional Statements

// Complex conditions
bool isValid = age >= 18 && hasLicense && !isSuspended;

if (isValid)
{
    // Allow driving
}

// Nested conditions (avoid when possible)
if (user != null)
{
    if (user.IsActive)
    {
        if (user.HasPermission("admin"))
        {
            // Do something
        }
    }
}

// Better: Guard clauses
if (user == null) return;
if (!user.IsActive) return;
if (!user.HasPermission("admin")) return;
// Do something

// Null-conditional in conditions
if (user?.IsActive == true)
{
    // Safe null check
}

Switch Statements and Expressions

// Traditional switch
switch (dayOfWeek)
{
    case DayOfWeek.Saturday:
    case DayOfWeek.Sunday:
        Console.WriteLine("Weekend");
        break;
    case DayOfWeek.Monday:
        Console.WriteLine("Start of week");
        break;
    default:
        Console.WriteLine("Weekday");
        break;
}

// Switch expression (C# 8+)
string message = dayOfWeek switch
{
    DayOfWeek.Saturday or DayOfWeek.Sunday => "Weekend",
    DayOfWeek.Monday => "Start of week",
    _ => "Weekday"
};

// With multiple values
int quarter = month switch
{
    1 or 2 or 3 => 1,
    4 or 5 or 6 => 2,
    7 or 8 or 9 => 3,
    10 or 11 or 12 => 4,
    _ => throw new ArgumentOutOfRangeException(nameof(month))
};

// Relational patterns
string category = score switch
{
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    >= 60 => "D",
    _ => "F"
};

Pattern Matching

// Type patterns
object value = GetValue();

if (value is string text)
{
    Console.WriteLine($"String length: {text.Length}");
}
else if (value is int number)
{
    Console.WriteLine($"Number: {number}");
}

// Switch with type patterns
string Describe(object obj) => obj switch
{
    null => "Nothing",
    string s => $"String of length {s.Length}",
    int n when n > 0 => $"Positive number: {n}",
    int n => $"Non-positive number: {n}",
    IEnumerable<int> list => $"List with {list.Count()} items",
    _ => $"Unknown type: {obj.GetType().Name}"
};

// Property patterns
bool IsWeekendAfternoon(DateTime dt) => dt is
{
    DayOfWeek: DayOfWeek.Saturday or DayOfWeek.Sunday,
    Hour: >= 12 and < 18
};

// Positional patterns (with deconstruction)
string DescribePoint(Point point) => point switch
{
    (0, 0) => "Origin",
    (var x, 0) => $"On X-axis at {x}",
    (0, var y) => $"On Y-axis at {y}",
    (var x, var y) when x == y => $"On diagonal at {x}",
    (var x, var y) => $"Point at ({x}, {y})"
};

Loop Variations

// for loop with multiple variables
for (int i = 0, j = 10; i < j; i++, j--)
{
    Console.WriteLine($"i={i}, j={j}");
}

// Infinite loop with break
while (true)
{
    var input = Console.ReadLine();
    if (input == "quit") break;
    Process(input);
}

// do-while (executes at least once)
string? userInput;
do
{
    Console.Write("Enter a number: ");
    userInput = Console.ReadLine();
} while (!int.TryParse(userInput, out _));

// foreach with index
foreach (var (item, index) in items.Select((x, i) => (x, i)))
{
    Console.WriteLine($"{index}: {item}");
}

// Parallel foreach (when order doesn't matter)
Parallel.ForEach(items, item =>
{
    ProcessItem(item);
});

break, continue, return

// break - exits the loop
for (int i = 0; i < 100; i++)
{
    if (i == 10) break;  // Exits at 10
    Console.WriteLine(i);
}

// continue - skips to next iteration
for (int i = 0; i < 10; i++)
{
    if (i % 2 == 0) continue;  // Skip even numbers
    Console.WriteLine(i);  // 1, 3, 5, 7, 9
}

// Labeled statements (rare, avoid if possible)
outerLoop:
for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (SomeCondition(i, j))
        {
            goto outerLoop;  // Breaks out of inner loop
        }
    }
}

// Better alternative: extract to method
bool ProcessMatrix()
{
    for (int i = 0; i < 10; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            if (SomeCondition(i, j))
            {
                return true;  // Clean exit
            }
        }
    }
    return false;
}

Guard Clauses and Early Returns

// Without guard clauses (deep nesting)
public decimal CalculateDiscount(Customer customer, Order order)
{
    if (customer != null)
    {
        if (customer.IsActive)
        {
            if (order != null)
            {
                if (order.Total > 100)
                {
                    return order.Total * 0.1m;
                }
            }
        }
    }
    return 0;
}

// With guard clauses (flat and readable)
public decimal CalculateDiscount(Customer? customer, Order? order)
{
    if (customer is null) return 0;
    if (!customer.IsActive) return 0;
    if (order is null) return 0;
    if (order.Total <= 100) return 0;

    return order.Total * 0.1m;
}

Interview Tips

Tip 1: Know when to use switch expressions vs traditional switch statements. Expressions are more concise for value-returning scenarios.

Tip 2: Understand that pattern matching can replace many if-else chains with more readable code.

Tip 3: Be ready to refactor nested conditions into guard clauses - this is a common interview task.

Common Interview Questions

  1. What's the difference between break and continue?

    • break exits the loop entirely. continue skips the rest of the current iteration and moves to the next. Both only affect the innermost loop they're in.
  2. When would you use a for loop vs foreach?

    • for: When you need the index, need to modify the collection, or iterate in reverse. foreach: When you just need to process each item. foreach is cleaner and prevents off-by-one errors.
  3. What is pattern matching in C#?

    • Pattern matching tests values against patterns and extracts data. Types: type patterns (is string s), property patterns (is { Length: > 0 }), relational patterns (is >= 10), positional patterns. Available in if statements and switch.
  4. What's the difference between switch statement and switch expression?

    • Statement: Multiple statements per case, needs break, no return value. Expression: Single expression per arm, returns a value, exhaustiveness checking, more concise. Use expressions when returning values.
  5. How do you exit nested loops?

    • Options: Labeled goto (avoid), extract to method and return, boolean flag, or restructure logic. Extracting to a method with early return is usually cleanest.
  6. What is a guard clause?

    • Early return that handles edge cases at the start of a method. Reduces nesting, improves readability, and makes the "happy path" clear. Common pattern: check for null, invalid input, or preconditions first.
  7. Explain the ternary operator.

    • condition ? valueIfTrue : valueIfFalse. Short form of if-else that returns a value. Keep it simple - if complex, use regular if-else for readability.