Ever felt your code is spiraling out of control, like a rollercoaster without brakes? Control statements are your safety rails, keeping the logic on track and making sure your application behaves as expected. If you’ve ever tangled yourself in nested ifs or messy loops, this post is for you.
Let’s demystify control statements in C# by walking through practical examples and best practices you can use right away.
Conditional Statements: if
, else if
, else
Conditional statements allow your program to make decisions based on conditions at runtime. These constructs are fundamental to controlling the logic flow of your application and are used extensively in nearly every program you write. They allow your application to adapt dynamically and execute different code paths based on user input, data values, or system state.
The most common forms include if
, else if
, and else
statements. As the conditions grow more complex, you can combine them with logical operators, use conditional (ternary) operators for succinctness, or even leverage modern features like pattern matching and null-conditional operators for safer and more readable code.
Let’s dive into each in more detail:
The if
Statement
int age = 20;
if (age >= 18)
{
Console.WriteLine("You are an adult.");
}
Explanation: Checks if age
is 18 or above and prints a message. Simple and readable.
The else
Statement
if (age >= 18)
{
Console.WriteLine("You are an adult.");
}
else
{
Console.WriteLine("You are a minor.");
}
Explanation: Provides an alternative path if the if
condition fails.
The else if
Statement
if (age < 13)
{
Console.WriteLine("You are a child.");
}
else if (age < 18)
{
Console.WriteLine("You are a teenager.");
}
else
{
Console.WriteLine("You are an adult.");
}
Explanation: Enables multiple condition checks in sequence.
Conditional Operator ?
string status = (age >= 18) ? "Adult" : "Minor";
Explanation: Shorter syntax for a simple if-else. It’s great for assigning values based on a condition.
Using Logical Operators
if (age > 18 && age < 60)
{
Console.WriteLine("Working age");
}
Explanation: Combine multiple conditions with &&
(and), ||
(or), !
(not). Logical operators are powerful for checking compound conditions.
Null-conditional Operators
string name = null;
int? length = name?.Length;
Explanation: Prevents null reference exceptions by safely accessing members only if the object is not null.
Pattern Matching in if
(C# 7+)
object obj = 42;
if (obj is int number)
{
Console.WriteLine($"Number: {number}");
}
Explanation: Declares and casts within the condition. Makes the code cleaner and avoids unsafe casts.
Best Practices
- Avoid deeply nested if-else blocks.
- Prefer early exits when possible to reduce nesting.
- Use pattern matching for cleaner type checks.
- Favor
switch
when you are checking the same variable across multiple values. - Use null-conditional operators to write safer code and avoid exceptions.
switch
Case
The switch
statement is another conditional control structure, particularly powerful when you need to compare a single variable against multiple possible values. Unlike if-else
, which checks arbitrary conditions, switch
simplifies multi-branching logic when you’re checking for equality against constant values.
With enhancements in recent C# versions (7 through 9), switch
has grown into a highly expressive and type-safe construct. You can now use pattern matching, tuple comparisons, relational and logical checks, and even expression-based syntax to write clean, declarative code.
Let’s break it down with real examples:
Traditional Syntax
int day = 3;
switch (day)
{
case 1:
Console.WriteLine("Monday");
break;
case 2:
Console.WriteLine("Tuesday");
break;
default:
Console.WriteLine("Other day");
break;
}
Explanation: This classic syntax is readable and effective for fixed value comparisons. Each case must include a break
to prevent fall-through.
Pattern Matching (C# 7+)
object val = 5;
switch (val)
{
case int i:
Console.WriteLine($"Integer: {i}");
break;
}
Explanation: Allows checking types and extracting values in one clean expression. This is particularly useful when working with object
types or needing runtime type inspection.
Switch Expressions (C# 8+)
string result = day switch
{
1 => "Monday",
2 => "Tuesday",
_ => "Other day"
};
Explanation: A compact and functional-style alternative to traditional switch
, especially useful for assignment and return values.
Property Pattern (C# 8+)
Person p = new Person { Age = 25 };
string category = p switch
{
{ Age: < 13 } => "Child",
{ Age: < 20 } => "Teenager",
_ => "Adult"
};
Explanation: Allows switching based on nested object properties, ideal for decision logic around complex objects.
Tuple Patterns (C# 8+)
(string, int) input = ("Bob", 30);
var output = input switch
{
("Bob", 30) => "Matched",
_ => "Not Matched"
};
Explanation: Pattern match on multiple values simultaneously. Great for handling state combinations or structured data.
Relational Patterns (C# 9+)
int temp = 25;
string state = temp switch
{
< 0 => "Freezing",
>= 0 and < 30 => "Cool",
_ => "Hot"
};
Explanation: Adds comparison-based logic directly inside the switch, avoiding extra if
checks.
Logical Patterns (C# 9+)
string label = temp switch
{
> 0 and < 100 => "Liquid",
_ => "Unknown"
};
Explanation: Combines logical operators with switch patterns to express complex rules concisely.
Using when
Clauses
switch (day)
{
case int d when d >= 1 && d <= 5:
Console.WriteLine("Weekday");
break;
}
Explanation: Refines a case by applying additional conditions. Useful when the same value could map to different behaviors.
Best Practices
- Prefer switch expressions for readability and conciseness.
- Use pattern matching to reduce boilerplate and enhance type safety.
- Always include a
default
case to handle unexpected input. - Group logically related cases to keep your code DRY and readable.
Loops: for
, while
, do-while
, foreach
Loops are the bread and butter of automation in programming. They allow you to repeat code execution without manual duplication, handling everything from array iteration to dynamic user input processing. Whether iterating a fixed number of times, evaluating a condition, or processing collections, C# offers a rich set of loop constructs tailored for each use case.
for
Loop
for (int i = 0; i < 5; i++)
{
Console.WriteLine(i);
}
Explanation: The for
loop is ideal when you know exactly how many times you want to execute a block of code. It includes initialization, a condition, and an iteration expression all in one line. This makes it perfect for numeric loops and index-based array operations.
while
Loop
int i = 0;
while (i < 5)
{
Console.WriteLine(i);
i++;
}
Explanation: Use a while
loop when the number of iterations is not known in advance and depends on dynamic conditions. It’s a pre-check loop, meaning it checks the condition before each execution.
do-while
Loop
int i = 0;
do
{
Console.WriteLine(i);
i++;
} while (i < 5);
Explanation: Unlike while
, a do-while
loop always executes the code block at least once because the condition is evaluated after the first iteration. Useful when an operation must happen before a condition is evaluated.
foreach
Loop
string[] fruits = {"apple", "banana"};
foreach (var fruit in fruits)
{
Console.WriteLine(fruit);
}
Explanation: Designed for iterating through collections without managing an index. It’s safer and cleaner than for
when you just need to access elements.
Infinite Loops
while (true)
{
// Loop forever unless break or return
break;
}
Explanation: Used in scenarios like game loops, background services, or event polling where you intentionally keep running until a specific condition or manual break is reached.
Nested Loops
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 2; j++)
{
Console.WriteLine($"i={i}, j={j}");
}
}
Explanation: Allows iteration across multi-dimensional data structures. Always monitor performance when nesting loops—complexity grows fast.
Multi-Dimensional Arrays
int[,] matrix = {{1, 2}, {3, 4}};
foreach (var val in matrix)
{
Console.WriteLine(val);
}
Explanation: Simplifies traversal of 2D arrays. However, if you need to know the indices, you should use nested for
loops instead.
The yield
Keyword
IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
}
Explanation: Lazily returns elements one by one, pausing the state of the method between calls. Great for streaming data or building pipelines.
Parallel Loops
Parallel.For(0, 10, i => Console.WriteLine(i));
Explanation: Executes iterations concurrently using threads. Best used for CPU-intensive operations. Be cautious with shared state and thread safety.
Best Practices & Insights
- Use
for
loops for indexed access and defined counts. - Favor
foreach
for readability and collection traversal. - Avoid logic that modifies a collection during
foreach
—it causes exceptions. - Use
while
when the end condition depends on real-time data. - Leverage
yield
for performance when dealing with large or delayed data sources. - Evaluate the overhead of
Parallel.For
before using—it’s powerful but not always efficient.
FAQ
switch
or if-else
?Use switch
for cleaner code when evaluating the same variable across multiple values.
Yes, it’s safer and more readable.
yield
?When you want to return a sequence without building an entire list in memory.
Conclusion: Control Your Code Like a Pro
Mastering control statements is like learning to drive stick—clunky at first, smooth later. The better you understand the flow of conditions and loops, the more elegant and efficient your C# code becomes.
Start small. Rewrite a nested if
using a switch. Try a pattern match instead of a cast. Experiment with yield
. Your future self (and teammates) will thank you.
Have a favorite trick with control statements? Share it in the comments below!