Skip to main content

2.6 - Control Structures

Control structures are essential elements in programming that direct the flow of execution based on certain conditions or sequences. They are fundamental to creating dynamic and responsive programs. In C#, control structures allow you to make decisions, repeat actions, and organize code execution in a logical manner.

🧩 Visual Learning: Understanding Program Flow​

When a program runs, it normally executes code line by line from top to bottom. Control structures change this normal flow, allowing your program to:

  1. Make decisions (conditional statements like if-else)
  2. Repeat actions (loops like for and while)
  3. Jump to different parts of the code (methods and function calls)

Think of your program like a road trip with a map:

START
β”‚
β–Ό
[Decision Point] ──Yes──► [Do Something]
β”‚ β”‚
No β”‚
β”‚ β”‚
β–Ό β–Ό
[Do Something Else] [Continue]
β”‚ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
END

πŸ’‘ Concept Breakdown: Why We Need Control Structures​

Imagine you're writing a program for a simple ATM:

  • Without control structures: The program would run the same way every time, regardless of the user's account balance or what they want to do.

  • With control structures: The program can:

    • Check if the user has enough money before allowing a withdrawal
    • Let the user choose between different options (deposit, withdraw, check balance)
    • Repeat actions (like asking for another transaction)

Control structures make your programs interactive and able to respond to different situations.


2.6.1 - Conditional Statements​

Conditional statements in C#, particularly if-else constructs, are pivotal for directing the flow of execution based on conditions. These statements evaluate Boolean expressions and execute the corresponding code blocks only when the conditions are true.

πŸ”° Beginner's Corner: Making Decisions in Code​

Conditional statements are how your program makes decisions, similar to how we make decisions in everyday life:

IF it's raining THEN
take an umbrella
ELSE
wear sunglasses

In C#, this looks like:

if (isRaining)
{
TakeUmbrella();
}
else
{
WearSunglasses();
}

The program checks if a condition is true (isRaining), and then executes different code based on the result. This is the foundation of creating programs that can respond to different situations.

2.6.1.1 - Basic If-Else Structure​

The basic syntax of an if-else statement consists of:

  • An if clause with a condition in parentheses
  • A code block to execute if the condition is true
  • Optional else if clauses with additional conditions
  • An optional else clause for when no conditions are met

Consider the scenario where you need to respond to varying temperature readings:

// Initialize the temperature variable
int temperature = 20;

// Evaluate the temperature and respond accordingly
// The code will execute the first block where the condition is true
if (temperature > 30)
{
// This block executes only if temperature is greater than 30
Console.WriteLine("Too hot");
}
else if (temperature < 0)
{
// This block executes only if temperature is less than 0
// Note: This will only be checked if the first condition was false
Console.WriteLine("It is freezing, below zero!");
}
else if (temperature < 10)
{
// This block executes only if temperature is less than 10 but not less than 0
// The order of conditions matters! If we put this before the temperature < 0 check,
// temperatures below 0 would trigger this block instead
Console.WriteLine("Too cold");
}
else
{
// This block executes if none of the above conditions are true
// In this case, when temperature is between 10 and 30 (inclusive)
Console.WriteLine("Just right");
}

2.6.1.2 - Modern Pattern Matching (C# 8.0+)​

C# 8.0 introduced enhanced pattern matching capabilities that make conditional logic more concise and expressive:

// Using pattern matching with switch expression (C# 8.0+)
// This accomplishes the same logic as the if-else chain above
string weatherCondition = temperature switch
{
// Each line represents a pattern to match and its result
> 30 => "Too hot", // If temperature > 30
< 0 => "It is freezing, below zero!", // If temperature < 0
< 10 => "Too cold", // If temperature < 10 (and not < 0)
_ => "Just right" // Default case (the underscore is a discard pattern)
};

// Display the result
Console.WriteLine($"Weather condition: {weatherCondition}");

2.6.1.3 - Compound Conditions​

You can combine multiple conditions using logical operators:

// Logical AND (&&) - both conditions must be true
if (temperature > 20 && temperature < 30)
{
Console.WriteLine("Warm and pleasant");
}

// Logical OR (||) - at least one condition must be true
if (temperature < 0 || temperature > 40)
{
Console.WriteLine("Extreme temperature!");
}

// Logical NOT (!) - inverts a condition
if (!(temperature >= 10 && temperature <= 30))
{
Console.WriteLine("Temperature is outside the comfortable range");
}

2.6.1.4 - Ternary Conditional Operator​

For simple conditional assignments, the ternary operator provides a compact syntax:

// condition ? valueIfTrue : valueIfFalse
string status = temperature > 25 ? "Warm" : "Cool";
Console.WriteLine($"It's {status} today");

Best Practices:

  • Use clear and concise conditions: Simplify your Boolean expressions as much as possible for better readability.
  • Maintain logical order: Place more specific conditions before more general ones to ensure correct evaluation.
  • Avoid deep nesting: Too many nested if-else statements can make your code hard to read and maintain. Consider refactoring into separate functions or using a switch statement where appropriate.
  • Use pattern matching: For complex conditional logic, consider using modern pattern matching features for cleaner code.

2.6.2 - The switch Statement​

The switch statement provides an efficient way to dispatch execution to different parts of code based on the value of an expression. It is particularly useful for handling multiple potential discrete values such as menu selections, specific command inputs, or known variable ranges.

2.6.2.1 - Traditional Switch Statement​

The traditional switch statement evaluates an expression once and compares it with a series of case values:

// Retrieve the current day of the week as a string
string dayOfWeek = DateTime.Now.DayOfWeek.ToString();

// Respond with a specific message based on the day
switch (dayOfWeek)
{
// Each case represents a possible value of dayOfWeek
case "Monday":
// This code executes if dayOfWeek equals "Monday"
Console.WriteLine("It's Monday!");
break; // The break statement exits the switch block

case "Tuesday":
Console.WriteLine("It's Tuesday!");
break;

case "Wednesday":
Console.WriteLine("It's Wednesday!");
break;

case "Thursday":
Console.WriteLine("It's Thursday!");
break;

case "Friday":
// We can have different code in each case
Console.WriteLine("TGIF! (Thank God It's Friday)");
break;

case "Saturday":
Console.WriteLine("It's Saturday! Time to relax.");
break;

case "Sunday":
Console.WriteLine("It's Sunday, enjoy your day off!");
break;

// The default case handles any value not explicitly covered above
default:
Console.WriteLine("Some other day");
break; // The last break is technically optional but recommended for consistency
}

2.6.2.2 - Fall-Through and Case Grouping​

Unlike some other languages, C# requires a break, return, or throw statement at the end of each case block to prevent fall-through. However, you can group multiple cases to share the same code block:

// Grouping cases that share the same implementation
switch (dayOfWeek)
{
// These cases share the same code block
case "Monday":
case "Tuesday":
case "Wednesday":
case "Thursday":
case "Friday":
Console.WriteLine("It's a weekday!");
break;

// These cases share a different code block
case "Saturday":
case "Sunday":
Console.WriteLine("It's the weekend!");
break;

default:
Console.WriteLine("Invalid day");
break;
}

2.6.2.3 - Pattern Matching in Switch Statements (C# 7.0+)​

C# 7.0 introduced pattern matching in switch statements, allowing for more complex conditions:

// Using pattern matching with different types
object item = 42; // Could be any type

switch (item)
{
// Type pattern - checks if item is an int and assigns it to n
case int n when n > 0:
Console.WriteLine($"Positive integer: {n}");
break;

case int n:
Console.WriteLine($"Non-positive integer: {n}");
break;

// Type pattern with string
case string s:
Console.WriteLine($"String of length {s.Length}");
break;

// null pattern
case null:
Console.WriteLine("Null value");
break;

default:
Console.WriteLine($"Other type: {item.GetType().Name}");
break;
}

2.6.2.4 - Switch Expressions (C# 8.0+)​

Switch expressions introduced in C# 8.0 offer a more concise syntax for cases that simply return a value based on a condition:

// Switch expression - more concise than traditional switch statements
// when you just need to compute a value based on a condition
var message = dayOfWeek switch
{
// Pattern => result format
"Monday" => "It's Monday, back to work!",
"Tuesday" => "It's Tuesday, keep going!",
"Wednesday" => "Midweek already!",
"Thursday" => "Almost there!",
"Friday" => "TGIF! (Thank God It's Friday)",
"Saturday" => "Weekend! Time to relax.",
"Sunday" => "Enjoy the rest!",

// The discard pattern (_) catches all other values
_ => "Day not recognized!"
};

// Use the computed value
Console.WriteLine(message);

2.6.2.5 - Property Patterns (C# 8.0+)​

Property patterns allow you to match against properties of an object:

// Define a simple class
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

// Create a person object
var person = new Person { Name = "Alice", Age = 25 };

// Use property pattern in a switch expression
string description = person switch
{
// Match based on property values
{ Age: < 18 } => "Minor",
{ Age: >= 18, Age: < 65 } => "Adult",
{ Age: >= 65 } => "Senior",
_ => "Unknown"
};

Console.WriteLine($"{person.Name} is a {description}");
Note

In C#, the underscore (_) used in the context of a switch expression acts as a discard, which effectively serves as a catch-all case. This means it will match any value that hasn't been explicitly matched by the preceding cases in the switch expression. It's akin to the default case in traditional switch statements.

Here's how it functions:

  • Catch-All Placeholder: The underscore (_) catches all values of the expression that do not match any other case pattern in the switch expression.
  • Prevents Compiler Errors: Without a discard or default case, the compiler would throw an error if none of the specified cases match because the switch expression must always return a value or handle all possible values of the input.
  • Improves Safety: It ensures that the switch expression is exhaustive, meaning that it accounts for all possible values of the input, thereby avoiding potential runtime errors.

2.6.2.6 - When to Use Switch vs. If-Else​

  • Use switch when:

    • You're comparing a single variable against multiple constant values
    • You have many possible execution paths based on a single value
    • The conditions are based on equality rather than ranges or complex logic
  • Use if-else when:

    • You have complex conditions involving multiple variables
    • Your conditions involve ranges rather than discrete values
    • You have only a few conditions to check

2.6.3 - Loops​

Loops are fundamental constructs in programming that allow repetitive execution of a block of code. They are particularly useful when a task needs to be repeated either a specified number of times or until a certain condition is met, or for iterating over the elements in collections.

2.6.3.1 - The for Loop​

The for loop is ideal for iterating over a sequence where the number of iterations is known before the loop starts. It consists of three parts: initialization, condition, and iteration statement.

// Example of a for loop that prints numbers from 0 to 4
for (int i = 0; i < 5; i++)
{
Console.WriteLine(i);
}

Key Components:

  • Initialization: int i = 0; - Sets up the loop variable.
  • Condition: i < 5; - Continues to loop as long as this condition is true.
  • Iteration Expression: i++ - Updates the loop variable each iteration.

2.6.3.2 - The while Loop​

A while loop executes the statements within it as long as the specified condition evaluates to true. It is particularly useful when the number of iterations is not known before the loop begins.

// Example of a while loop that counts down from 5 to 1
int x = 5;

while (x > 0)
{
Console.WriteLine(x);
x--;
}

Usage Note:

  • The condition is checked before each iteration, so the loop's body may not execute at all if the condition is initially false.

2.6.3.3 - The do-while Loop​

The do-while loop is similar to the while loop, but it guarantees that the code block within the loop executes at least once. This is because the condition is checked after each iteration, not before.

// Example of a do-while loop that asks for a password until it's correct
string password = "";

do
{
Console.Write("Enter password: ");
password = Console.ReadLine();

if (password != "secret")
{
Console.WriteLine("Incorrect password. Try again.");
}
} while (password != "secret");

Console.WriteLine("Access granted!");

Usage Note:

  • The do-while loop is useful when you need to ensure that a block of code is executed at least once, even if the condition might be false initially.
  • In the example above, the loop continues to prompt for the password until the user enters the correct one ("secret").

2.6.3.4 - The foreach Loop​

The foreach loop is designed for iterating over collections that implement the IEnumerable interface. It is the preferred method for looping through items in an array or a collection because it simplifies the syntax and avoids the possibility of an index out of bounds error.

// Example of a foreach loop that prints each character in a string
string greeting = "Hello";

foreach (char c in greeting)
{
Console.WriteLine(c);
}

Benefits:

  • Automatically handles the iteration over the entire collection, eliminating the need for an indexing variable.
  • Enhances readability and reduces the chance of programming errors such as off-by-one errors.

2.6.3.5 - Choosing the Right Loop​

Each type of loop has its ideal use case:

  • Use a for loop when the number of iterations is known.
  • Opt for a while loop when the continuation condition is based on dynamic data during execution.
  • Employ a foreach loop when working with elements in a collection or array to ensure simplicity and readability.

Understanding and selecting the appropriate loop type can greatly influence the clarity, efficiency, and performance of your code.


2.6.4 - Jump Statements​

Jump statements like break, continue, return, and goto can alter the flow of control by exiting loops, skipping iterations, or redirecting control to another part of the program.

2.6.4.1 - The break Statement​

The break statement terminates the execution of the nearest enclosing loop or switch statement.

// Using break to exit a loop early
for (int i = 0; i < 10; i++)
{
if (i == 5)
{
// Exit the loop when i equals 5
Console.WriteLine("Breaking the loop at i = 5");
break;
}
Console.WriteLine($"Current value: {i}");
}
// Execution continues here after the break
Console.WriteLine("Loop finished");

2.6.4.2 - The continue Statement​

The continue statement skips the current iteration of a loop and proceeds to the next iteration.

// Using continue to skip specific iterations
for (int i = 0; i < 10; i++)
{
if (i % 2 == 0)
{
// Skip even numbers
continue;
}
Console.WriteLine($"Odd number: {i}");
}

2.6.4.3 - The return Statement​

The return statement exits the current method and optionally returns a value to the caller.

// Using return to exit a method early
public bool IsPositive(int number)
{
if (number <= 0)
{
// Exit the method early with a result
return false;
}

// More code could be here...

// Return the final result
return true;
}

2.6.4.4 - The goto Statement​

The goto statement transfers control to a labeled statement. While it's generally discouraged in modern programming due to its potential to create confusing "spaghetti code," it can be useful in specific scenarios like state machines or breaking out of nested loops.

// Using goto to break out of nested loops
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (i == 1 && j == 1)
{
Console.WriteLine("Found target at i=1, j=1");
goto LoopExit; // Jump to the labeled statement
}
Console.WriteLine($"Checking i={i}, j={j}");
}
}

LoopExit: // Label for the goto statement
Console.WriteLine("Exited both loops");

Best Practices:

  • Use break and continue judiciously to make your code more readable and efficient.
  • Prefer early returns for guard clauses and validation checks.
  • Avoid goto in most cases; there are usually cleaner alternatives.
  • Consider extracting complex logic into separate methods instead of using jump statements.

2.6.5 - Nested Loops​

Nested loops are a sequence of loops where one loop is contained within the body of another. They are particularly useful when working with multi-dimensional arrays, tables, or any application where you need to perform operations on a grid or matrix-like structure.

2.6.5.1 - Nested for Loops​

Nested for loops are commonly used for accessing two-dimensional arrays or performing operations that require a combination of elements from different sequences.

// Create a 3x3 matrix
int[,] matrix = new int[3,3];

// Initialize the matrix with values (i+j)
// The outer loop iterates through rows
for (int i = 0; i < 3; i++)
{
// The inner loop iterates through columns for each row
for (int j = 0; j < 3; j++)
{
// Set each element to the sum of its indices
matrix[i, j] = i + j;

// This code executes 3Γ—3 = 9 times in total
}
}

// Print the matrix in a grid format
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
// Write each element followed by a space (no line break)
Console.Write(matrix[i, j] + " ");
}
// After each row is printed, add a line break
Console.WriteLine();
}

2.6.5.2 - Nested while Loops​

While nested while loops are less common, they are used when the number of iterations for each loop is determined dynamically.

// Example: Finding prime number pairs (p, p+2) where both are prime
int p = 3;
int count = 0;
int limit = 5; // Find 5 pairs

// Outer loop continues until we've found enough pairs
while (count < limit)
{
bool pIsPrime = true;
int i = 2;

// Inner loop checks if p is prime
while (i * i <= p)
{
if (p % i == 0)
{
pIsPrime = false;
break;
}
i++;
}

// If p is prime, check if p+2 is also prime
if (pIsPrime)
{
int p2 = p + 2;
bool p2IsPrime = true;
i = 2;

// Another inner loop checks if p+2 is prime
while (i * i <= p2)
{
if (p2 % i == 0)
{
p2IsPrime = false;
break;
}
i++;
}

// If both are prime, we found a pair
if (p2IsPrime)
{
Console.WriteLine($"Found prime pair: ({p}, {p2})");
count++;
}
}

p += 2; // Check only odd numbers
}

2.6.5.3 - Nested foreach Loops​

Nested foreach loops are particularly useful when dealing with collections of collections, such as lists of lists or any enumerable collections.

// Create a list of lists (similar to a jagged array)
List<List<int>> listOfLists = new List<List<int>>
{
new List<int> {1, 2, 3},
new List<int> {4, 5, 6},
new List<int> {7, 8, 9}
};

// Outer loop iterates through each sublist
foreach (List<int> sublist in listOfLists)
{
// Inner loop iterates through each item in the current sublist
foreach (int item in sublist)
{
// Process each individual item
Console.Write(item + " ");
}
// After processing each sublist, add a line break
Console.WriteLine();
}

2.6.5.4 - Performance Considerations​

Nested loops multiply the number of iterations, which can lead to performance issues with large datasets:

  • A single loop with n iterations has O(n) time complexity
  • Two nested loops have O(nΒ²) time complexity
  • Three nested loops have O(nΒ³) time complexity

For large values of n, deeply nested loops can become performance bottlenecks. Always consider whether there are more efficient algorithms or data structures that could accomplish the same task with fewer iterations.

Best Practices:

  • Keep the most computationally intensive operations in the innermost loop
  • Consider breaking out of inner loops early when possible
  • For large datasets, look for algorithms that avoid nested loops
  • Use meaningful variable names for loop counters to improve readability
  • Consider extracting the inner loop body to a separate method for complex operations